mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Response Ops][Alerting] Using alertsClient
for stack monitoring rule types to write default alerts-as-data docs (#174169)
Towards https://github.com/elastic/response-ops-team/issues/164 Resolves https://github.com/elastic/kibana/issues/167436 ## Summary * Switches these rule types to use `alertsClient` from alerting framework in favor of the deprecated `alertFactory` * Defines the `default` alert config for these rule types so framework level fields will be written out into the `.alerts-default.alerts-default` index with no rule type specific fields. * Updated terminology from `alert` to `rule` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
7a4f08a831
commit
c8d0ae5b9f
50 changed files with 4454 additions and 2899 deletions
|
@ -546,33 +546,89 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_cluster_health 1`] = `undefined`;
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_cluster_health 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_cpu_usage 1`] = `undefined`;
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_cpu_usage 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_disk_usage 1`] = `undefined`;
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_disk_usage 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_elasticsearch_version_mismatch 1`] = `undefined`;
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_elasticsearch_version_mismatch 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_jvm_memory_usage 1`] = `undefined`;
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_jvm_memory_usage 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_kibana_version_mismatch 1`] = `undefined`;
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_kibana_version_mismatch 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_license_expiration 1`] = `undefined`;
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_license_expiration 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_logstash_version_mismatch 1`] = `undefined`;
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_logstash_version_mismatch 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_missing_monitoring_data 1`] = `undefined`;
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_missing_monitoring_data 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_nodes_changed 1`] = `undefined`;
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_nodes_changed 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_thread_pool_search_rejections 1`] = `undefined`;
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_thread_pool_search_rejections 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_thread_pool_write_rejections 1`] = `undefined`;
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_thread_pool_write_rejections 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_ccr_read_exceptions 1`] = `undefined`;
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_ccr_read_exceptions 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_shard_size 1`] = `undefined`;
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: monitoring_shard_size 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: observability.rules.custom_threshold 1`] = `
|
||||
Object {
|
||||
|
|
|
@ -1,202 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CCRReadExceptionsRule } from './ccr_read_exceptions_rule';
|
||||
import { RULE_CCR_READ_EXCEPTIONS } from '../../common/constants';
|
||||
import { fetchCCRReadExceptions } from '../lib/alerts/fetch_ccr_read_exceptions';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
type ICCRReadExceptionsRuleMock = CCRReadExceptionsRule & {
|
||||
defaultParams: {
|
||||
duration: string;
|
||||
};
|
||||
} & {
|
||||
actionVariables: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_ccr_read_exceptions', () => ({
|
||||
fetchCCRReadExceptions: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('CCRReadExceptionsRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock;
|
||||
expect(rule.ruleOptions.id).toBe(RULE_CCR_READ_EXCEPTIONS);
|
||||
expect(rule.ruleOptions.name).toBe('CCR read exceptions');
|
||||
expect(rule.ruleOptions.throttle).toBe('6h');
|
||||
expect(rule.ruleOptions.defaultParams).toStrictEqual({
|
||||
duration: '1h',
|
||||
});
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{
|
||||
name: 'remoteCluster',
|
||||
description: 'The remote cluster experiencing CCR read exceptions.',
|
||||
},
|
||||
{
|
||||
name: 'followerIndex',
|
||||
description: 'The follower index reporting CCR read exceptions.',
|
||||
},
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
describe('execute', () => {
|
||||
const FakeDate = function () {};
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const nodeId = 'myNodeId';
|
||||
const nodeName = 'myNodeName';
|
||||
const remoteCluster = 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1';
|
||||
const followerIndex = '.follower_index_1';
|
||||
const leaderIndex = '.leader_index_1';
|
||||
const readExceptions = [
|
||||
{
|
||||
exception: {
|
||||
type: 'read_exceptions_type_1',
|
||||
reason: 'read_exceptions_reason_1',
|
||||
},
|
||||
},
|
||||
];
|
||||
const stat = {
|
||||
remoteCluster,
|
||||
followerIndex,
|
||||
leaderIndex,
|
||||
read_exceptions: readExceptions,
|
||||
clusterUuid,
|
||||
nodeId,
|
||||
nodeName,
|
||||
};
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
alertFactory: {
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Date = FakeDate as DateConstructor;
|
||||
(fetchCCRReadExceptions as jest.Mock).mockImplementation(() => {
|
||||
return [stat];
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
});
|
||||
|
||||
it('should fire actions', async () => {
|
||||
const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock;
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Current 'follower_index' index affected: ${followerIndex}. [View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Verify follower and leader index relationships on the affected remote cluster.`,
|
||||
action: `[View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain:
|
||||
'Verify follower and leader index relationships on the affected remote cluster.',
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
remoteCluster,
|
||||
remoteClusters: remoteCluster,
|
||||
followerIndex,
|
||||
followerIndices: followerIndex,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchCCRReadExceptions as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat,
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock;
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Current 'follower_index' index affected: ${followerIndex}. [View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`,
|
||||
internalShortMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Verify follower and leader index relationships on the affected remote cluster.`,
|
||||
action: `[View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`,
|
||||
actionPlain:
|
||||
'Verify follower and leader index relationships on the affected remote cluster.',
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
remoteCluster,
|
||||
remoteClusters: remoteCluster,
|
||||
followerIndex,
|
||||
followerIndices: followerIndex,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,257 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CpuUsageRule } from './cpu_usage_rule';
|
||||
import { RULE_CPU_USAGE } from '../../common/constants';
|
||||
import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_cpu_usage_node_stats', () => ({
|
||||
fetchCpuUsageNodeStats: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('CpuUsageRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new CpuUsageRule();
|
||||
expect(rule.ruleOptions.id).toBe(RULE_CPU_USAGE);
|
||||
expect(rule.ruleOptions.name).toBe('CPU Usage');
|
||||
expect(rule.ruleOptions.throttle).toBe('1d');
|
||||
expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' });
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'node', description: 'The node reporting high cpu usage.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
function FakeDate() {}
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const nodeId = 'myNodeId';
|
||||
const nodeName = 'myNodeName';
|
||||
const cpuUsage = 91;
|
||||
const stat = {
|
||||
clusterUuid,
|
||||
nodeId,
|
||||
nodeName,
|
||||
cpuUsage,
|
||||
};
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
alertFactory: {
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
Date = FakeDate;
|
||||
(fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [stat];
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
});
|
||||
|
||||
it('should fire actions', async () => {
|
||||
const rule = new CpuUsageRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
ccs: undefined,
|
||||
cluster: { clusterUuid, clusterName },
|
||||
cpuUsage,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
clusterUuid,
|
||||
cpuUsage,
|
||||
nodeId,
|
||||
nodeName,
|
||||
},
|
||||
nodeId,
|
||||
nodeName,
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: `Node #start_link${nodeName}#end_link is reporting cpu usage of ${cpuUsage}% at #absolute`,
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkCheck hot threads#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/cluster-nodes-hot-threads.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkCheck long running tasks#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/tasks.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes/myNodeId',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify CPU level of node.`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: 'Verify CPU level of node.',
|
||||
clusterName,
|
||||
count,
|
||||
nodes: `${nodeName}:${cpuUsage}`,
|
||||
node: `${nodeName}:${cpuUsage}`,
|
||||
state: 'firing',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fire actions if under threshold', async () => {
|
||||
(fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat,
|
||||
cpuUsage: 1,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new CpuUsageRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [],
|
||||
});
|
||||
expect(scheduleActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat,
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new CpuUsageRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`,
|
||||
internalShortMessage: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify CPU level of node.`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`,
|
||||
actionPlain: 'Verify CPU level of node.',
|
||||
clusterName,
|
||||
count,
|
||||
nodes: `${nodeName}:${cpuUsage}`,
|
||||
node: `${nodeName}:${cpuUsage}`,
|
||||
state: 'firing',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,180 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DiskUsageRule } from './disk_usage_rule';
|
||||
import { RULE_DISK_USAGE } from '../../common/constants';
|
||||
import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
type IDiskUsageAlertMock = DiskUsageRule & {
|
||||
defaultParams: {
|
||||
threshold: number;
|
||||
duration: string;
|
||||
};
|
||||
} & {
|
||||
actionVariables: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_disk_usage_node_stats', () => ({
|
||||
fetchDiskUsageNodeStats: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DiskUsageRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const alert = new DiskUsageRule() as IDiskUsageAlertMock;
|
||||
expect(alert.ruleOptions.id).toBe(RULE_DISK_USAGE);
|
||||
expect(alert.ruleOptions.name).toBe('Disk Usage');
|
||||
expect(alert.ruleOptions.throttle).toBe('1d');
|
||||
expect(alert.ruleOptions.defaultParams).toStrictEqual({ threshold: 80, duration: '5m' });
|
||||
expect(alert.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'node', description: 'The node reporting high disk usage.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
const FakeDate = function () {};
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const nodeId = 'myNodeId';
|
||||
const nodeName = 'myNodeName';
|
||||
const diskUsage = 91;
|
||||
const stat = {
|
||||
clusterUuid,
|
||||
nodeId,
|
||||
nodeName,
|
||||
diskUsage,
|
||||
};
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
alertFactory: {
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Date = FakeDate as DateConstructor;
|
||||
(fetchDiskUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [stat];
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
});
|
||||
|
||||
it('should fire actions', async () => {
|
||||
const rule = new DiskUsageRule() as IDiskUsageAlertMock;
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify disk usage level of node.`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: 'Verify disk usage level of node.',
|
||||
clusterName,
|
||||
count,
|
||||
nodes: `${nodeName}:${diskUsage}`,
|
||||
node: `${nodeName}:${diskUsage}`,
|
||||
state: 'firing',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchDiskUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat,
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new DiskUsageRule() as IDiskUsageAlertMock;
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`,
|
||||
internalShortMessage: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify disk usage level of node.`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/myNodeId?_g=(cluster_uuid:abc123,ccs:testCluster))`,
|
||||
actionPlain: 'Verify disk usage level of node.',
|
||||
clusterName,
|
||||
count,
|
||||
nodes: `${nodeName}:${diskUsage}`,
|
||||
node: `${nodeName}:${diskUsage}`,
|
||||
state: 'firing',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,176 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LargeShardSizeRule } from './large_shard_size_rule';
|
||||
import { RULE_LARGE_SHARD_SIZE } from '../../common/constants';
|
||||
import { fetchIndexShardSize } from '../lib/alerts/fetch_index_shard_size';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
type ILargeShardSizeRuleMock = LargeShardSizeRule & {
|
||||
defaultParams: {
|
||||
threshold: number;
|
||||
duration: string;
|
||||
};
|
||||
} & {
|
||||
actionVariables: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_index_shard_size', () => ({
|
||||
fetchIndexShardSize: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('LargeShardSizeRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock;
|
||||
expect(rule.ruleOptions.id).toBe(RULE_LARGE_SHARD_SIZE);
|
||||
expect(rule.ruleOptions.name).toBe('Shard size');
|
||||
expect(rule.ruleOptions.throttle).toBe('12h');
|
||||
expect(rule.ruleOptions.defaultParams).toStrictEqual({
|
||||
threshold: 55,
|
||||
indexPattern: '-.*',
|
||||
});
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'shardIndex', description: 'The index experiencing large average shard size.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
describe('execute', () => {
|
||||
const FakeDate = function () {};
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const shardIndex = 'apm-8.0.0-onboarding-2021.06.30';
|
||||
const shardSize = 0;
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const stat = {
|
||||
shardIndex,
|
||||
shardSize,
|
||||
clusterUuid,
|
||||
};
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
alertFactory: {
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Date = FakeDate as DateConstructor;
|
||||
(fetchIndexShardSize as jest.Mock).mockImplementation(() => {
|
||||
return [stat];
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
});
|
||||
|
||||
it('should fire actions', async () => {
|
||||
const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock;
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `Large shard size alert is firing for the following index: ${shardIndex}. [View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `Large shard size alert is firing for the following index: ${shardIndex}. Investigate indices with large shard sizes.`,
|
||||
action: `[View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: 'Investigate indices with large shard sizes.',
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
shardIndex,
|
||||
shardIndices: shardIndex,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchIndexShardSize as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat,
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock;
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `Large shard size alert is firing for the following index: ${shardIndex}. [View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`,
|
||||
internalShortMessage: `Large shard size alert is firing for the following index: ${shardIndex}. Investigate indices with large shard sizes.`,
|
||||
action: `[View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`,
|
||||
actionPlain: 'Investigate indices with large shard sizes.',
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
shardIndex,
|
||||
shardIndices: shardIndex,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,250 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LicenseExpirationRule } from './license_expiration_rule';
|
||||
import { RULE_LICENSE_EXPIRATION } from '../../common/constants';
|
||||
import { AlertSeverity } from '../../common/enums';
|
||||
import { fetchLicenses } from '../lib/alerts/fetch_licenses';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_licenses', () => ({
|
||||
fetchLicenses: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
config: {
|
||||
ui: {
|
||||
show_license_expiration: true,
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('LicenseExpirationRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new LicenseExpirationRule();
|
||||
expect(rule.ruleOptions.id).toBe(RULE_LICENSE_EXPIRATION);
|
||||
expect(rule.ruleOptions.name).toBe('License expiration');
|
||||
expect(rule.ruleOptions.throttle).toBe('1d');
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'expiredDate', description: 'The date when the license expires.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the license belong.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
function FakeDate() {}
|
||||
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const license = {
|
||||
status: 'expired',
|
||||
type: 'gold',
|
||||
expiryDateMS: 1000 * 60 * 60 * 24 * 59,
|
||||
clusterUuid,
|
||||
};
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
alertFactory: {
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
Date = FakeDate;
|
||||
(fetchLicenses as jest.Mock).mockImplementation(() => {
|
||||
return [license];
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should fire actions', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-03-30T00:00:00.000Z'));
|
||||
const alert = new LicenseExpirationRule();
|
||||
const type = alert.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: alert.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid, clusterName },
|
||||
ccs: undefined,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
clusterUuid: 'abc123',
|
||||
expiryDateMS: 5097600000,
|
||||
status: 'expired',
|
||||
type: 'gold',
|
||||
},
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#relative',
|
||||
type: 'time',
|
||||
isRelative: true,
|
||||
isAbsolute: false,
|
||||
timestamp: 5097600000,
|
||||
},
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 5097600000,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'license',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1680134400000,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
action: '[Please update your license.](elasticsearch/nodes)',
|
||||
actionPlain: 'Please update your license.',
|
||||
internalFullMessage:
|
||||
'License expiration alert is firing for testCluster. Your license expires in 53 years. [Please update your license.](elasticsearch/nodes)',
|
||||
internalShortMessage:
|
||||
'License expiration alert is firing for testCluster. Your license expires in 53 years. Please update your license.',
|
||||
clusterName,
|
||||
expiredDate: '53 years',
|
||||
state: 'firing',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fire actions if the license is not expired', async () => {
|
||||
(fetchLicenses as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
status: 'active',
|
||||
type: 'gold',
|
||||
expiryDateMS: 1000 * 60 * 60 * 24 * 61,
|
||||
clusterUuid,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new LicenseExpirationRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).not.toHaveBeenCalledWith({});
|
||||
expect(scheduleActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use danger severity for a license expiring soon', async () => {
|
||||
(fetchLicenses as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
status: 'active',
|
||||
type: 'gold',
|
||||
expiryDateMS: 1000 * 60 * 60 * 24 * 2,
|
||||
clusterUuid,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new LicenseExpirationRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Danger);
|
||||
});
|
||||
|
||||
it('should use warning severity for a license expiring in a bit', async () => {
|
||||
(fetchLicenses as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
status: 'active',
|
||||
type: 'gold',
|
||||
expiryDateMS: 1000 * 60 * 60 * 24 * 31,
|
||||
clusterUuid,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new LicenseExpirationRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Warning);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,291 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { MemoryUsageRule } from './memory_usage_rule';
|
||||
import { RULE_MEMORY_USAGE } from '../../common/constants';
|
||||
import { fetchMemoryUsageNodeStats } from '../lib/alerts/fetch_memory_usage_node_stats';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_memory_usage_node_stats', () => ({
|
||||
fetchMemoryUsageNodeStats: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('MemoryUsageRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new MemoryUsageRule();
|
||||
expect(rule.ruleOptions.id).toBe(RULE_MEMORY_USAGE);
|
||||
expect(rule.ruleOptions.name).toBe('Memory Usage (JVM)');
|
||||
expect(rule.ruleOptions.throttle).toBe('1d');
|
||||
expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' });
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'node', description: 'The node reporting high memory usage.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
function FakeDate() {}
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const nodeId = 'myNodeId';
|
||||
const nodeName = 'myNodeName';
|
||||
const memoryUsage = 91;
|
||||
const stat = {
|
||||
clusterUuid,
|
||||
nodeId,
|
||||
nodeName,
|
||||
memoryUsage,
|
||||
};
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
alertFactory: {
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
Date = FakeDate;
|
||||
(fetchMemoryUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [stat];
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
});
|
||||
|
||||
it('should fire actions', async () => {
|
||||
const rule = new MemoryUsageRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
ccs: undefined,
|
||||
cluster: { clusterUuid, clusterName },
|
||||
memoryUsage,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
clusterUuid,
|
||||
memoryUsage,
|
||||
nodeId,
|
||||
nodeName,
|
||||
},
|
||||
nodeId,
|
||||
nodeName,
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: `Node #start_link${nodeName}#end_link is reporting JVM memory usage of ${memoryUsage}% at #absolute`,
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkTune thread pools#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkManaging ES Heap#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl: '{elasticWebsiteUrl}blog/a-heap-of-trouble',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkIdentify large indices/shards#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/indices',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkAdd more data nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkResize your deployment (ECE)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes/myNodeId',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify memory usage level of node.`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: 'Verify memory usage level of node.',
|
||||
clusterName,
|
||||
count,
|
||||
nodes: `${nodeName}:${memoryUsage}.00`,
|
||||
node: `${nodeName}:${memoryUsage}.00`,
|
||||
state: 'firing',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fire actions if under threshold', async () => {
|
||||
(fetchMemoryUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat,
|
||||
memoryUsage: 1,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new MemoryUsageRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [],
|
||||
});
|
||||
expect(scheduleActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchMemoryUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat,
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new MemoryUsageRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`,
|
||||
internalShortMessage: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify memory usage level of node.`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`,
|
||||
actionPlain: 'Verify memory usage level of node.',
|
||||
clusterName,
|
||||
count,
|
||||
nodes: `${nodeName}:${memoryUsage}.00`,
|
||||
node: `${nodeName}:${memoryUsage}.00`,
|
||||
state: 'firing',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,248 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { MissingMonitoringDataRule } from './missing_monitoring_data_rule';
|
||||
import { RULE_MISSING_MONITORING_DATA } from '../../common/constants';
|
||||
import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_missing_monitoring_data', () => ({
|
||||
fetchMissingMonitoringData: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
show_license_expiration: true,
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('MissingMonitoringDataRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new MissingMonitoringDataRule();
|
||||
expect(rule.ruleOptions.id).toBe(RULE_MISSING_MONITORING_DATA);
|
||||
expect(rule.ruleOptions.name).toBe('Missing monitoring data');
|
||||
expect(rule.ruleOptions.throttle).toBe('6h');
|
||||
expect(rule.ruleOptions.defaultParams).toStrictEqual({ limit: '1d', duration: '15m' });
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'node', description: 'The node missing monitoring data.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
function FakeDate() {}
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const nodeId = 'esNode1';
|
||||
const nodeName = 'esName1';
|
||||
const gapDuration = 3000001;
|
||||
const missingData = [
|
||||
{
|
||||
nodeId,
|
||||
nodeName,
|
||||
clusterUuid,
|
||||
gapDuration,
|
||||
},
|
||||
];
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
alertFactory: {
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
Date = FakeDate;
|
||||
(fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
|
||||
return missingData;
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
});
|
||||
|
||||
it('should fire actions', async () => {
|
||||
const rule = new MissingMonitoringDataRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
ccs: undefined,
|
||||
cluster: { clusterUuid, clusterName },
|
||||
nodeId,
|
||||
nodeName,
|
||||
gapDuration,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
clusterUuid,
|
||||
gapDuration,
|
||||
limit: 86400000,
|
||||
nodeId,
|
||||
nodeName,
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'For the past an hour, we have not detected any monitoring data from the Elasticsearch node: esName1, starting at #absolute',
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkView all Elasticsearch nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Verify monitoring settings on the node',
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `We have not detected any monitoring data for node ${nodeName} in cluster: ${clusterName}. [View what monitoring data we do have for this node.](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `We have not detected any monitoring data for node ${nodeName} in cluster: ${clusterName}. Verify the node is up and running, then double check the monitoring settings.`,
|
||||
nodes: `node: ${nodeName}`,
|
||||
node: `node: ${nodeName}`,
|
||||
action: `[View what monitoring data we do have for this node.](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain:
|
||||
'Verify the node is up and running, then double check the monitoring settings.',
|
||||
clusterName,
|
||||
count,
|
||||
state: 'firing',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fire actions if under threshold', async () => {
|
||||
(fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...missingData[0],
|
||||
gapDuration: 1,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new MissingMonitoringDataRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [],
|
||||
});
|
||||
expect(scheduleActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...missingData[0],
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new MissingMonitoringDataRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `We have not detected any monitoring data for node ${nodeName} in cluster: ${clusterName}. [View what monitoring data we do have for this node.](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`,
|
||||
internalShortMessage: `We have not detected any monitoring data for node ${nodeName} in cluster: ${clusterName}. Verify the node is up and running, then double check the monitoring settings.`,
|
||||
nodes: `node: ${nodeName}`,
|
||||
node: `node: ${nodeName}`,
|
||||
action: `[View what monitoring data we do have for this node.](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`,
|
||||
actionPlain:
|
||||
'Verify the node is up and running, then double check the monitoring settings.',
|
||||
clusterName,
|
||||
count,
|
||||
state: 'firing',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,297 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ThreadPoolSearchRejectionsRule } from './thread_pool_search_rejections_rule';
|
||||
import { RULE_THREAD_POOL_SEARCH_REJECTIONS } from '../../common/constants';
|
||||
import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_thread_pool_rejections_stats', () => ({
|
||||
fetchThreadPoolRejectionStats: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
show_license_expiration: true,
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ThreadpoolSearchRejectionsRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new ThreadPoolSearchRejectionsRule();
|
||||
expect(rule.ruleOptions.id).toBe(RULE_THREAD_POOL_SEARCH_REJECTIONS);
|
||||
expect(rule.ruleOptions.name).toBe('Thread pool search rejections');
|
||||
expect(rule.ruleOptions.throttle).toBe('1d');
|
||||
expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' });
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'node', description: 'The node reporting high thread pool search rejections.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
describe('execute', () => {
|
||||
function FakeDate() {}
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const nodeId = 'esNode1';
|
||||
const nodeName = 'esName1';
|
||||
const threadPoolType = 'search';
|
||||
const rejectionCount = 400;
|
||||
const stat = [
|
||||
{
|
||||
rejectionCount,
|
||||
type: threadPoolType,
|
||||
clusterUuid,
|
||||
nodeId,
|
||||
nodeName,
|
||||
ccs: null,
|
||||
},
|
||||
];
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
alertFactory: {
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
Date = FakeDate;
|
||||
(fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => {
|
||||
return stat;
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
});
|
||||
|
||||
it('should fire actions', async () => {
|
||||
const rule = new ThreadPoolSearchRejectionsRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
ccs: null,
|
||||
cluster: { clusterUuid, clusterName },
|
||||
nodeId,
|
||||
nodeName,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
rejectionCount,
|
||||
clusterUuid,
|
||||
type: threadPoolType,
|
||||
nodeId,
|
||||
nodeName,
|
||||
ccs: null,
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: `Node #start_link${nodeName}#end_link is reporting ${rejectionCount} ${threadPoolType} rejections at #absolute`,
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkMonitor this node#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes/esNode1/advanced',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkOptimize complex queries#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}blog/advanced-tuning-finding-and-fixing-slow-elasticsearch-queries',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkAdd more nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkResize your deployment (ECE)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkThread pool settings#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: `elasticsearch/nodes/${nodeId}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
node: `${nodeName}`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: `Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
clusterName,
|
||||
count: 1,
|
||||
threadPoolType,
|
||||
state: 'firing',
|
||||
});
|
||||
});
|
||||
it('should not fire actions if under threshold', async () => {
|
||||
(fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat[0],
|
||||
rejectionCount: 1,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new ThreadPoolSearchRejectionsRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [],
|
||||
});
|
||||
expect(scheduleActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat[0],
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new ThreadPoolSearchRejectionsRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`,
|
||||
internalShortMessage: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
node: `${nodeName}`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/esNode1?_g=(cluster_uuid:abc123,ccs:testCluster))`,
|
||||
actionPlain: `Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
clusterName,
|
||||
count,
|
||||
state: 'firing',
|
||||
threadPoolType,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,297 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ThreadPoolWriteRejectionsRule } from './thread_pool_write_rejections_rule';
|
||||
import { RULE_THREAD_POOL_WRITE_REJECTIONS } from '../../common/constants';
|
||||
import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_thread_pool_rejections_stats', () => ({
|
||||
fetchThreadPoolRejectionStats: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
show_license_expiration: true,
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ThreadpoolWriteRejectionsAlert', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new ThreadPoolWriteRejectionsRule();
|
||||
expect(rule.ruleOptions.id).toBe(RULE_THREAD_POOL_WRITE_REJECTIONS);
|
||||
expect(rule.ruleOptions.name).toBe(`Thread pool write rejections`);
|
||||
expect(rule.ruleOptions.throttle).toBe('1d');
|
||||
expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' });
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'node', description: 'The node reporting high thread pool write rejections.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
describe('execute', () => {
|
||||
function FakeDate() {}
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const nodeId = 'esNode1';
|
||||
const nodeName = 'esName1';
|
||||
const threadPoolType = 'write';
|
||||
const rejectionCount = 400;
|
||||
const stat = [
|
||||
{
|
||||
rejectionCount,
|
||||
type: threadPoolType,
|
||||
clusterUuid,
|
||||
nodeId,
|
||||
nodeName,
|
||||
ccs: null,
|
||||
},
|
||||
];
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
alertFactory: {
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
Date = FakeDate;
|
||||
(fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => {
|
||||
return stat;
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
});
|
||||
|
||||
it('should fire actions', async () => {
|
||||
const rule = new ThreadPoolWriteRejectionsRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
ccs: null,
|
||||
cluster: { clusterUuid, clusterName },
|
||||
nodeId,
|
||||
nodeName,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
rejectionCount,
|
||||
clusterUuid,
|
||||
type: threadPoolType,
|
||||
nodeId,
|
||||
nodeName,
|
||||
ccs: null,
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: `Node #start_link${nodeName}#end_link is reporting ${rejectionCount} ${threadPoolType} rejections at #absolute`,
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkMonitor this node#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes/esNode1/advanced',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkOptimize complex queries#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}blog/advanced-tuning-finding-and-fixing-slow-elasticsearch-queries',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkAdd more nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkResize your deployment (ECE)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkThread pool settings#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: `elasticsearch/nodes/${nodeId}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
node: `${nodeName}`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: `Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
clusterName,
|
||||
count: 1,
|
||||
threadPoolType,
|
||||
state: 'firing',
|
||||
});
|
||||
});
|
||||
it('should not fire actions if under threshold', async () => {
|
||||
(fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat[0],
|
||||
rejectionCount: 1,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new ThreadPoolWriteRejectionsRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [],
|
||||
});
|
||||
expect(scheduleActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat[0],
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new ThreadPoolWriteRejectionsRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`,
|
||||
internalShortMessage: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
node: `${nodeName}`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/esNode1?_g=(cluster_uuid:abc123,ccs:testCluster))`,
|
||||
actionPlain: `Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
clusterName,
|
||||
count,
|
||||
state: 'firing',
|
||||
threadPoolType,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { RulesClient } from '@kbn/alerting-plugin/server';
|
||||
import { AlertInstanceState } from '../../../common/types/alerts';
|
||||
import { AlertsFactory } from '../../alerts';
|
||||
import { RulesFactory } from '../../rules';
|
||||
import { CommonAlertState, CommonAlertFilter, RulesByType } from '../../../common/types/alerts';
|
||||
import { RULES } from '../../../common/constants';
|
||||
|
||||
|
@ -18,7 +18,7 @@ export async function fetchStatus(
|
|||
filters: CommonAlertFilter[] = []
|
||||
): Promise<RulesByType> {
|
||||
const rulesByType = await Promise.all(
|
||||
(alertTypes || RULES).map(async (type) => AlertsFactory.getByType(type, rulesClient))
|
||||
(alertTypes || RULES).map(async (type) => RulesFactory.getByType(type, rulesClient))
|
||||
);
|
||||
if (!rulesByType.length) return {};
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { coreMock } from '@kbn/core/server/mocks';
|
||||
import { MonitoringPlugin } from './plugin';
|
||||
import { AlertsFactory } from './alerts';
|
||||
import { RulesFactory } from './rules';
|
||||
|
||||
jest.mock('./es_client/instantiate_client', () => ({
|
||||
instantiateClient: jest.fn().mockImplementation(() => ({
|
||||
|
@ -71,10 +71,10 @@ describe('Monitoring plugin', () => {
|
|||
expect(plugin['bulkUploader']).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should register all alerts', async () => {
|
||||
const alerts = AlertsFactory.getAll();
|
||||
it('should register all rules', async () => {
|
||||
const rules = RulesFactory.getAll();
|
||||
const plugin = new MonitoringPlugin(initializerContext as any);
|
||||
await plugin.setup(coreSetup as any, setupPlugins as any);
|
||||
expect(setupPlugins.alerting.registerType).toHaveBeenCalledTimes(alerts.length);
|
||||
expect(setupPlugins.alerting.registerType).toHaveBeenCalledTimes(rules.length);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,7 +30,7 @@ import {
|
|||
LOGGING_TAG,
|
||||
SAVED_OBJECT_TELEMETRY,
|
||||
} from '../common/constants';
|
||||
import { AlertsFactory } from './alerts';
|
||||
import { RulesFactory } from './rules';
|
||||
import { configSchema, createConfig, MonitoringConfig } from './config';
|
||||
import { instantiateClient } from './es_client/instantiate_client';
|
||||
import { initBulkUploader } from './kibana_monitoring';
|
||||
|
@ -123,9 +123,9 @@ export class MonitoringPlugin
|
|||
setupPlugins: this.setupPlugins!,
|
||||
});
|
||||
|
||||
const alerts = AlertsFactory.getAll();
|
||||
for (const alert of alerts) {
|
||||
plugins.alerting?.registerType(alert.getRuleType());
|
||||
const rules = RulesFactory.getAll();
|
||||
for (const rule of rules) {
|
||||
plugins.alerting?.registerType(rule.getRuleType());
|
||||
}
|
||||
|
||||
const config = createConfig(this.initializerContext.config.get<TypeOf<typeof configSchema>>());
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { ActionResult } from '@kbn/actions-plugin/server';
|
||||
import { RuleTypeParams, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import { ALERT_ACTION_TYPE_LOG } from '../../../../../common/constants';
|
||||
import { AlertsFactory } from '../../../../alerts';
|
||||
import { RulesFactory } from '../../../../rules';
|
||||
import { handleError } from '../../../../lib/errors';
|
||||
import { MonitoringCore, RouteDependencies } from '../../../../types';
|
||||
|
||||
|
@ -26,7 +26,7 @@ export function enableAlertsRoute(server: MonitoringCore, npRoute: RouteDependen
|
|||
const infraContext = await context.infra;
|
||||
const actionContext = await context.actions;
|
||||
|
||||
const alerts = AlertsFactory.getAll();
|
||||
const alerts = RulesFactory.getAll();
|
||||
if (alerts.length) {
|
||||
const { isSufficientlySecure, hasPermanentEncryptionKey } = npRoute.alerting
|
||||
?.getSecurityHealth
|
||||
|
|
|
@ -7,15 +7,23 @@
|
|||
|
||||
import { Logger, ElasticsearchClient, DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
|
||||
import {
|
||||
RuleType,
|
||||
RuleNotifyWhen,
|
||||
RuleExecutorOptions,
|
||||
Alert,
|
||||
RulesClient,
|
||||
RuleExecutorServices,
|
||||
DEFAULT_AAD_CONFIG,
|
||||
AlertsClientError,
|
||||
} from '@kbn/alerting-plugin/server';
|
||||
import { Rule, RuleTypeParams, RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import {
|
||||
Rule,
|
||||
RuleTypeParams,
|
||||
RawAlertInstance,
|
||||
SanitizedRule,
|
||||
AlertInstanceContext,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { ActionsClient } from '@kbn/actions-plugin/server';
|
||||
import { parseDuration } from '@kbn/alerting-plugin/common';
|
||||
import {
|
||||
|
@ -78,7 +86,16 @@ export class BaseRule {
|
|||
this.scopedLogger = Globals.app.getLogger(ruleOptions.id);
|
||||
}
|
||||
|
||||
public getRuleType(): RuleType<never, never, never, never, never, 'default'> {
|
||||
public getRuleType(): RuleType<
|
||||
never,
|
||||
never,
|
||||
ExecutedState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
never,
|
||||
DefaultAlert
|
||||
> {
|
||||
const { id, name, actionVariables } = this.ruleOptions;
|
||||
return {
|
||||
id,
|
||||
|
@ -95,15 +112,21 @@ export class BaseRule {
|
|||
minimumLicenseRequired: 'basic',
|
||||
isExportable: false,
|
||||
executor: (
|
||||
options: RuleExecutorOptions<never, never, AlertInstanceState, never, 'default'> & {
|
||||
state: ExecutedState;
|
||||
}
|
||||
options: RuleExecutorOptions<
|
||||
never,
|
||||
ExecutedState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>
|
||||
): Promise<any> => this.execute(options),
|
||||
category: DEFAULT_APP_CATEGORIES.management.id,
|
||||
producer: 'monitoring',
|
||||
actionVariables: {
|
||||
context: actionVariables,
|
||||
},
|
||||
alerts: DEFAULT_AAD_CONFIG,
|
||||
// As there is "[key: string]: unknown;" in CommonAlertParams,
|
||||
// we couldn't figure out a schema for validation and created a follow on issue:
|
||||
// https://github.com/elastic/kibana/issues/153754
|
||||
|
@ -230,13 +253,23 @@ export class BaseRule {
|
|||
services,
|
||||
params,
|
||||
state,
|
||||
}: RuleExecutorOptions<never, never, AlertInstanceState, never, 'default'> & {
|
||||
state: ExecutedState;
|
||||
}): Promise<any> {
|
||||
}: RuleExecutorOptions<
|
||||
never,
|
||||
ExecutedState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>): Promise<any> {
|
||||
this.scopedLogger.debug(
|
||||
`Executing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}`
|
||||
);
|
||||
|
||||
const { alertsClient } = services;
|
||||
if (!alertsClient) {
|
||||
throw new AlertsClientError();
|
||||
}
|
||||
|
||||
const esClient = services.scopedClusterClient.asCurrentUser;
|
||||
const clusters = await this.fetchClusters(esClient, params as CommonAlertParams);
|
||||
const data = await this.fetchData(params, esClient, clusters);
|
||||
|
@ -270,7 +303,12 @@ export class BaseRule {
|
|||
protected async processData(
|
||||
data: AlertData[],
|
||||
clusters: AlertCluster[],
|
||||
services: RuleExecutorServices<AlertInstanceState, never, 'default'>,
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
state: ExecutedState
|
||||
) {
|
||||
const currentUTC = +new Date();
|
||||
|
@ -287,9 +325,6 @@ export class BaseRule {
|
|||
for (const node of nodes) {
|
||||
const newAlertStates: AlertNodeState[] = [];
|
||||
// quick fix for now so that non node level alerts will use the cluster id
|
||||
const instance = services.alertFactory.create(
|
||||
node.meta.nodeId || node.meta.instanceId || cluster.clusterUuid
|
||||
);
|
||||
|
||||
if (node.shouldFire) {
|
||||
const { meta } = node;
|
||||
|
@ -309,13 +344,19 @@ export class BaseRule {
|
|||
nodeState.ui.message = this.getUiMessage(nodeState, node);
|
||||
// store the state of each node in array.
|
||||
newAlertStates.push(nodeState);
|
||||
}
|
||||
const alertInstanceState = { alertStates: newAlertStates };
|
||||
// update the alert's state with the new node states
|
||||
instance.replaceState(alertInstanceState);
|
||||
if (newAlertStates.length) {
|
||||
this.executeActions(instance, alertInstanceState, null, cluster);
|
||||
state.lastExecutedAction = currentUTC;
|
||||
|
||||
const alertInstanceState = { alertStates: newAlertStates };
|
||||
// update the alert's state with the new node states
|
||||
if (newAlertStates.length) {
|
||||
const alertId = node.meta.nodeId || node.meta.instanceId || cluster.clusterUuid;
|
||||
services.alertsClient?.report({
|
||||
id: alertId,
|
||||
actionGroup: 'default',
|
||||
state: alertInstanceState,
|
||||
});
|
||||
this.executeActions(services, alertId, alertInstanceState, null, cluster);
|
||||
state.lastExecutedAction = currentUTC;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -346,8 +387,14 @@ export class BaseRule {
|
|||
}
|
||||
|
||||
protected executeActions(
|
||||
instance: Alert,
|
||||
instanceState: AlertInstanceState | AlertState | unknown,
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
alertId: string,
|
||||
alertState: AlertInstanceState | AlertState | unknown,
|
||||
item: AlertData | unknown,
|
||||
cluster?: AlertCluster | unknown
|
||||
) {
|
|
@ -0,0 +1,473 @@
|
|||
/*
|
||||
* 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 { CCRReadExceptionsRule } from './ccr_read_exceptions_rule';
|
||||
import { RULE_CCR_READ_EXCEPTIONS } from '../../common/constants';
|
||||
import { fetchCCRReadExceptions } from '../lib/alerts/fetch_ccr_read_exceptions';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
|
||||
type ICCRReadExceptionsRuleMock = CCRReadExceptionsRule & {
|
||||
defaultParams: {
|
||||
duration: string;
|
||||
};
|
||||
} & {
|
||||
actionVariables: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_ccr_read_exceptions', () => ({
|
||||
fetchCCRReadExceptions: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('CCRReadExceptionsRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock;
|
||||
expect(rule.ruleOptions.id).toBe(RULE_CCR_READ_EXCEPTIONS);
|
||||
expect(rule.ruleOptions.name).toBe('CCR read exceptions');
|
||||
expect(rule.ruleOptions.throttle).toBe('6h');
|
||||
expect(rule.ruleOptions.defaultParams).toStrictEqual({
|
||||
duration: '1h',
|
||||
});
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{
|
||||
name: 'remoteCluster',
|
||||
description: 'The remote cluster experiencing CCR read exceptions.',
|
||||
},
|
||||
{
|
||||
name: 'followerIndex',
|
||||
description: 'The follower index reporting CCR read exceptions.',
|
||||
},
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
describe('execute', () => {
|
||||
const FakeDate = function () {};
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const nodeId = 'myNodeId';
|
||||
const nodeName = 'myNodeName';
|
||||
const remoteCluster = 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1';
|
||||
const followerIndex = '.follower_index_1';
|
||||
const leaderIndex = '.leader_index_1';
|
||||
const readExceptions = [
|
||||
{
|
||||
exception: {
|
||||
type: 'read_exceptions_type_1',
|
||||
reason: 'read_exceptions_reason_1',
|
||||
},
|
||||
},
|
||||
];
|
||||
const stat = {
|
||||
remoteCluster,
|
||||
followerIndex,
|
||||
leaderIndex,
|
||||
read_exceptions: readExceptions,
|
||||
clusterUuid,
|
||||
nodeId,
|
||||
nodeName,
|
||||
};
|
||||
|
||||
const services = alertsMock.createRuleExecutorServices();
|
||||
|
||||
const executorOptions = {
|
||||
services,
|
||||
state: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Date = FakeDate as DateConstructor;
|
||||
(fetchCCRReadExceptions as jest.Mock).mockImplementation(() => {
|
||||
return [stat];
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fire action', async () => {
|
||||
const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock;
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
actionGroup: 'default',
|
||||
id: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: undefined,
|
||||
cluster: { clusterName: 'testCluster', clusterUuid: 'abc123' },
|
||||
itemLabel: '.follower_index_1',
|
||||
meta: {
|
||||
followerIndex: '.follower_index_1',
|
||||
instanceId: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1',
|
||||
itemLabel: '.follower_index_1',
|
||||
lastReadException: undefined,
|
||||
leaderIndex: '.leader_index_1',
|
||||
remoteCluster: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1',
|
||||
shardId: undefined,
|
||||
},
|
||||
nodeId: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1',
|
||||
nodeName: '.follower_index_1',
|
||||
ui: {
|
||||
isFiring: true,
|
||||
lastCheckedMS: 0,
|
||||
message: {
|
||||
code: undefined,
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkIdentify CCR usage/stats#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
startToken: '#start_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/ccr',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkManage CCR follower indices#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{basePath}management/data/cross_cluster_replication/follower_indices',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkCreate auto-follow patterns#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{basePath}management/data/cross_cluster_replication/auto_follow_patterns',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkAdd follower index API (Docs)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/ccr-put-follow.html',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkCross-cluster replication (Docs)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/xpack-ccr.html',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkBi-directional replication (Blog)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}blog/bi-directional-replication-with-elasticsearch-cross-cluster-replication-ccr',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkFollow the Leader (Blog)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}blog/follow-the-leader-an-introduction-to-cross-cluster-replication-in-elasticsearch',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
text: 'Follower index #start_link.follower_index_1#end_link is reporting CCR read exceptions on remote cluster: BcK-0pmsQniyPQfZuauuXw_remote_cluster_1 at #absolute',
|
||||
tokens: [
|
||||
{
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
startToken: '#absolute',
|
||||
timestamp: 1,
|
||||
type: 'time',
|
||||
},
|
||||
{
|
||||
endToken: '#end_link',
|
||||
startToken: '#start_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/ccr/.follower_index_1/shard/undefined',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1',
|
||||
context: {
|
||||
internalFullMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Current 'follower_index' index affected: ${followerIndex}. [View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Verify follower and leader index relationships on the affected remote cluster.`,
|
||||
action: `[View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain:
|
||||
'Verify follower and leader index relationships on the affected remote cluster.',
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
remoteCluster,
|
||||
remoteClusters: remoteCluster,
|
||||
followerIndex,
|
||||
followerIndices: followerIndex,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Verify follower and leader index relationships on the affected remote cluster.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchCCRReadExceptions as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat,
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock;
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
actionGroup: 'default',
|
||||
id: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: 'testCluster',
|
||||
cluster: { clusterName: 'testCluster', clusterUuid: 'abc123' },
|
||||
itemLabel: '.follower_index_1',
|
||||
meta: {
|
||||
followerIndex: '.follower_index_1',
|
||||
instanceId: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1',
|
||||
itemLabel: '.follower_index_1',
|
||||
lastReadException: undefined,
|
||||
leaderIndex: '.leader_index_1',
|
||||
remoteCluster: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1',
|
||||
shardId: undefined,
|
||||
},
|
||||
nodeId: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1',
|
||||
nodeName: '.follower_index_1',
|
||||
ui: {
|
||||
isFiring: true,
|
||||
lastCheckedMS: 0,
|
||||
message: {
|
||||
code: undefined,
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkIdentify CCR usage/stats#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
startToken: '#start_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/ccr',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkManage CCR follower indices#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{basePath}management/data/cross_cluster_replication/follower_indices',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkCreate auto-follow patterns#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{basePath}management/data/cross_cluster_replication/auto_follow_patterns',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkAdd follower index API (Docs)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/ccr-put-follow.html',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkCross-cluster replication (Docs)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/xpack-ccr.html',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkBi-directional replication (Blog)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}blog/bi-directional-replication-with-elasticsearch-cross-cluster-replication-ccr',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkFollow the Leader (Blog)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}blog/follow-the-leader-an-introduction-to-cross-cluster-replication-in-elasticsearch',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
text: 'Follower index #start_link.follower_index_1#end_link is reporting CCR read exceptions on remote cluster: BcK-0pmsQniyPQfZuauuXw_remote_cluster_1 at #absolute',
|
||||
tokens: [
|
||||
{
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
startToken: '#absolute',
|
||||
timestamp: 1,
|
||||
type: 'time',
|
||||
},
|
||||
{
|
||||
endToken: '#end_link',
|
||||
startToken: '#start_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/ccr/.follower_index_1/shard/undefined',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1',
|
||||
context: {
|
||||
internalFullMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Current 'follower_index' index affected: ${followerIndex}. [View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`,
|
||||
internalShortMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Verify follower and leader index relationships on the affected remote cluster.`,
|
||||
action: `[View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`,
|
||||
actionPlain:
|
||||
'Verify follower and leader index relationships on the affected remote cluster.',
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
remoteCluster,
|
||||
remoteClusters: remoteCluster,
|
||||
followerIndex,
|
||||
followerIndices: followerIndex,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Verify follower and leader index relationships on the affected remote cluster.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,9 +7,11 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { Alert } from '@kbn/alerting-plugin/server';
|
||||
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { parseDuration } from '@kbn/alerting-plugin/common/parse_duration';
|
||||
import { SanitizedRule, RawAlertInstance } from '@kbn/alerting-plugin/common';
|
||||
import { SanitizedRule, RawAlertInstance, AlertInstanceContext } from '@kbn/alerting-plugin/common';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { BaseRule } from './base_rule';
|
||||
import {
|
||||
AlertData,
|
||||
|
@ -209,7 +211,13 @@ export class CCRReadExceptionsRule extends BaseRule {
|
|||
}
|
||||
|
||||
protected executeActions(
|
||||
instance: Alert,
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
alertId: string,
|
||||
{ alertStates }: AlertInstanceState,
|
||||
item: AlertData | null,
|
||||
cluster: AlertCluster
|
||||
|
@ -261,21 +269,27 @@ export class CCRReadExceptionsRule extends BaseRule {
|
|||
}
|
||||
);
|
||||
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage,
|
||||
internalFullMessage,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
remoteCluster,
|
||||
followerIndex,
|
||||
/* continue to send "remoteClusters" and "followerIndices" values for users still using it though
|
||||
services.alertsClient?.setAlertData({
|
||||
id: alertId,
|
||||
context: {
|
||||
internalShortMessage,
|
||||
internalFullMessage,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
remoteCluster,
|
||||
followerIndex,
|
||||
/* continue to send "remoteClusters" and "followerIndices" values for users still using it though
|
||||
we have replaced it with "remoteCluster" and "followerIndex" in the template due to alerts per index instead of all indices
|
||||
see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431
|
||||
*/
|
||||
remoteClusters: remoteCluster,
|
||||
followerIndices: followerIndex,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
remoteClusters: remoteCluster,
|
||||
followerIndices: followerIndex,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: internalShortMessage,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -10,7 +10,8 @@ import { RULE_CLUSTER_HEALTH } from '../../common/constants';
|
|||
import { AlertClusterHealthType, AlertSeverity } from '../../common/enums';
|
||||
import { fetchClusterHealth } from '../lib/alerts/fetch_cluster_health';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
|
@ -75,24 +76,8 @@ describe('ClusterHealthRule', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
alertFactory: {
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
const services = alertsMock.createRuleExecutorServices();
|
||||
const executorOptions = { services, state: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
|
@ -107,66 +92,77 @@ describe('ClusterHealthRule', () => {
|
|||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fire actions', async () => {
|
||||
it('should fire action', async () => {
|
||||
const rule = new ClusterHealthRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: {},
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' },
|
||||
ccs,
|
||||
itemLabel: undefined,
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
meta: {
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' },
|
||||
ccs,
|
||||
clusterUuid,
|
||||
health: AlertClusterHealthType.Yellow,
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'Elasticsearch cluster health is yellow.',
|
||||
nextSteps: [
|
||||
{
|
||||
text: 'Allocate missing replica shards. #start_linkView now#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/indices',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
itemLabel: undefined,
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
meta: {
|
||||
ccs,
|
||||
clusterUuid,
|
||||
health: AlertClusterHealthType.Yellow,
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'Elasticsearch cluster health is yellow.',
|
||||
nextSteps: [
|
||||
{
|
||||
text: 'Allocate missing replica shards. #start_linkView now#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/indices',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: AlertSeverity.Warning,
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
severity: AlertSeverity.Warning,
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
action: '[Allocate missing replica shards.](elasticsearch/indices)',
|
||||
actionPlain: 'Allocate missing replica shards.',
|
||||
internalFullMessage:
|
||||
'Cluster health alert is firing for testCluster. Current health is yellow. [Allocate missing replica shards.](elasticsearch/indices)',
|
||||
internalShortMessage:
|
||||
'Cluster health alert is firing for testCluster. Current health is yellow. Allocate missing replica shards.',
|
||||
clusterName,
|
||||
clusterHealth: 'yellow',
|
||||
state: 'firing',
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
context: {
|
||||
action: '[Allocate missing replica shards.](elasticsearch/indices)',
|
||||
actionPlain: 'Allocate missing replica shards.',
|
||||
internalFullMessage:
|
||||
'Cluster health alert is firing for testCluster. Current health is yellow. [Allocate missing replica shards.](elasticsearch/indices)',
|
||||
internalShortMessage:
|
||||
'Cluster health alert is firing for testCluster. Current health is yellow. Allocate missing replica shards.',
|
||||
clusterName,
|
||||
clusterHealth: 'yellow',
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]:
|
||||
'Cluster health alert is firing for testCluster. Current health is yellow. Allocate missing replica shards.',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -186,8 +182,8 @@ describe('ClusterHealthRule', () => {
|
|||
...executorOptions,
|
||||
params: {},
|
||||
} as any);
|
||||
expect(replaceState).not.toHaveBeenCalledWith({});
|
||||
expect(scheduleActions).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.report).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.setAlertData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { Alert } from '@kbn/alerting-plugin/server';
|
||||
import { SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { AlertInstanceContext, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { BaseRule } from './base_rule';
|
||||
import {
|
||||
AlertData,
|
||||
|
@ -115,8 +117,14 @@ export class ClusterHealthRule extends BaseRule {
|
|||
};
|
||||
}
|
||||
|
||||
protected async executeActions(
|
||||
instance: Alert,
|
||||
protected executeActions(
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
alertId: string,
|
||||
{ alertStates }: AlertInstanceState,
|
||||
item: AlertData | null,
|
||||
cluster: AlertCluster
|
||||
|
@ -139,34 +147,41 @@ export class ClusterHealthRule extends BaseRule {
|
|||
});
|
||||
|
||||
const action = `[${actionText}](elasticsearch/indices)`;
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage',
|
||||
{
|
||||
defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {actionText}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
health,
|
||||
actionText,
|
||||
},
|
||||
}
|
||||
),
|
||||
internalFullMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage',
|
||||
{
|
||||
defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {action}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
health,
|
||||
action,
|
||||
},
|
||||
}
|
||||
),
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
clusterHealth: health,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: actionText,
|
||||
const internalShortMessage = i18n.translate(
|
||||
'xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage',
|
||||
{
|
||||
defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {actionText}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
health,
|
||||
actionText,
|
||||
},
|
||||
}
|
||||
);
|
||||
services.alertsClient?.setAlertData({
|
||||
id: alertId,
|
||||
context: {
|
||||
internalShortMessage,
|
||||
internalFullMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage',
|
||||
{
|
||||
defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {action}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
health,
|
||||
action,
|
||||
},
|
||||
}
|
||||
),
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
clusterHealth: health,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: actionText,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: internalShortMessage,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
334
x-pack/plugins/monitoring/server/rules/cpu_usage_rule.test.ts
Normal file
334
x-pack/plugins/monitoring/server/rules/cpu_usage_rule.test.ts
Normal file
|
@ -0,0 +1,334 @@
|
|||
/*
|
||||
* 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 { CpuUsageRule } from './cpu_usage_rule';
|
||||
import { RULE_CPU_USAGE } from '../../common/constants';
|
||||
import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_cpu_usage_node_stats', () => ({
|
||||
fetchCpuUsageNodeStats: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('CpuUsageRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new CpuUsageRule();
|
||||
expect(rule.ruleOptions.id).toBe(RULE_CPU_USAGE);
|
||||
expect(rule.ruleOptions.name).toBe('CPU Usage');
|
||||
expect(rule.ruleOptions.throttle).toBe('1d');
|
||||
expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' });
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'node', description: 'The node reporting high cpu usage.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
function FakeDate() {}
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const nodeId = 'myNodeId';
|
||||
const nodeName = 'myNodeName';
|
||||
const cpuUsage = 91;
|
||||
const stat = {
|
||||
clusterUuid,
|
||||
nodeId,
|
||||
nodeName,
|
||||
cpuUsage,
|
||||
};
|
||||
|
||||
const services = alertsMock.createRuleExecutorServices();
|
||||
const executorOptions = {
|
||||
services,
|
||||
state: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
Date = FakeDate;
|
||||
(fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [stat];
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fire action', async () => {
|
||||
const rule = new CpuUsageRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
actionGroup: 'default',
|
||||
id: 'myNodeId',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: undefined,
|
||||
cluster: { clusterUuid, clusterName },
|
||||
cpuUsage,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
clusterUuid,
|
||||
cpuUsage,
|
||||
nodeId,
|
||||
nodeName,
|
||||
},
|
||||
nodeId,
|
||||
nodeName,
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: `Node #start_link${nodeName}#end_link is reporting cpu usage of ${cpuUsage}% at #absolute`,
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkCheck hot threads#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/cluster-nodes-hot-threads.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkCheck long running tasks#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/tasks.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes/myNodeId',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'myNodeId',
|
||||
context: {
|
||||
internalFullMessage: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify CPU level of node.`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: 'Verify CPU level of node.',
|
||||
clusterName,
|
||||
count,
|
||||
nodes: `${nodeName}:${cpuUsage}`,
|
||||
node: `${nodeName}:${cpuUsage}`,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify CPU level of node.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fire actions if under threshold', async () => {
|
||||
(fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat,
|
||||
cpuUsage: 1,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new CpuUsageRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.setAlertData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat,
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new CpuUsageRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
actionGroup: 'default',
|
||||
id: 'myNodeId',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: 'testCluster',
|
||||
cluster: { clusterUuid, clusterName },
|
||||
cpuUsage,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
ccs: 'testCluster',
|
||||
clusterUuid,
|
||||
cpuUsage,
|
||||
nodeId,
|
||||
nodeName,
|
||||
},
|
||||
nodeId,
|
||||
nodeName,
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: `Node #start_link${nodeName}#end_link is reporting cpu usage of ${cpuUsage}% at #absolute`,
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkCheck hot threads#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/cluster-nodes-hot-threads.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkCheck long running tasks#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/tasks.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes/myNodeId',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'myNodeId',
|
||||
context: {
|
||||
internalFullMessage: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`,
|
||||
internalShortMessage: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify CPU level of node.`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`,
|
||||
actionPlain: 'Verify CPU level of node.',
|
||||
clusterName,
|
||||
count,
|
||||
nodes: `${nodeName}:${cpuUsage}`,
|
||||
node: `${nodeName}:${cpuUsage}`,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify CPU level of node.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,9 +8,11 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { Alert } from '@kbn/alerting-plugin/server';
|
||||
import { RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { AlertInstanceContext, RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import { parseDuration } from '@kbn/alerting-plugin/common/parse_duration';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { BaseRule } from './base_rule';
|
||||
import {
|
||||
AlertData,
|
||||
|
@ -144,7 +146,13 @@ export class CpuUsageRule extends BaseRule {
|
|||
}
|
||||
|
||||
protected executeActions(
|
||||
instance: Alert,
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
alertId: string,
|
||||
{ alertStates }: AlertInstanceState,
|
||||
item: AlertData | null,
|
||||
cluster: AlertCluster
|
||||
|
@ -191,19 +199,25 @@ export class CpuUsageRule extends BaseRule {
|
|||
},
|
||||
}
|
||||
);
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage,
|
||||
internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
/* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544
|
||||
see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431
|
||||
*/
|
||||
nodes: `${firingNode.nodeName}:${firingNode.cpuUsage}`,
|
||||
count: 1,
|
||||
node: `${firingNode.nodeName}:${firingNode.cpuUsage}`,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
services.alertsClient?.setAlertData({
|
||||
id: alertId,
|
||||
context: {
|
||||
internalShortMessage,
|
||||
internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
/* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544
|
||||
see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431
|
||||
*/
|
||||
nodes: `${firingNode.nodeName}:${firingNode.cpuUsage}`,
|
||||
count: 1,
|
||||
node: `${firingNode.nodeName}:${firingNode.cpuUsage}`,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: internalShortMessage,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
400
x-pack/plugins/monitoring/server/rules/disk_usage_rule.test.ts
Normal file
400
x-pack/plugins/monitoring/server/rules/disk_usage_rule.test.ts
Normal file
|
@ -0,0 +1,400 @@
|
|||
/*
|
||||
* 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 { DiskUsageRule } from './disk_usage_rule';
|
||||
import { RULE_DISK_USAGE } from '../../common/constants';
|
||||
import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
|
||||
type IDiskUsageAlertMock = DiskUsageRule & {
|
||||
defaultParams: {
|
||||
threshold: number;
|
||||
duration: string;
|
||||
};
|
||||
} & {
|
||||
actionVariables: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_disk_usage_node_stats', () => ({
|
||||
fetchDiskUsageNodeStats: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DiskUsageRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const alert = new DiskUsageRule() as IDiskUsageAlertMock;
|
||||
expect(alert.ruleOptions.id).toBe(RULE_DISK_USAGE);
|
||||
expect(alert.ruleOptions.name).toBe('Disk Usage');
|
||||
expect(alert.ruleOptions.throttle).toBe('1d');
|
||||
expect(alert.ruleOptions.defaultParams).toStrictEqual({ threshold: 80, duration: '5m' });
|
||||
expect(alert.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'node', description: 'The node reporting high disk usage.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
const FakeDate = function () {};
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const nodeId = 'myNodeId';
|
||||
const nodeName = 'myNodeName';
|
||||
const diskUsage = 91;
|
||||
const stat = {
|
||||
clusterUuid,
|
||||
nodeId,
|
||||
nodeName,
|
||||
diskUsage,
|
||||
};
|
||||
|
||||
const services = alertsMock.createRuleExecutorServices();
|
||||
const executorOptions = { services, state: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
Date = FakeDate as DateConstructor;
|
||||
(fetchDiskUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [stat];
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fire action', async () => {
|
||||
const rule = new DiskUsageRule() as IDiskUsageAlertMock;
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'myNodeId',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: undefined,
|
||||
cluster: {
|
||||
clusterName: 'testCluster',
|
||||
clusterUuid: 'abc123',
|
||||
},
|
||||
diskUsage: 91,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
clusterUuid: 'abc123',
|
||||
diskUsage: 91,
|
||||
nodeId: 'myNodeId',
|
||||
nodeName: 'myNodeName',
|
||||
},
|
||||
nodeId: 'myNodeId',
|
||||
nodeName: 'myNodeName',
|
||||
ui: {
|
||||
isFiring: true,
|
||||
lastCheckedMS: 0,
|
||||
message: {
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkTune for disk usage#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/tune-for-disk-usage.html',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkIdentify large indices#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
startToken: '#start_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/indices',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkImplement ILM policies#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/index-lifecycle-management.html',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkAdd more data nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkResize your deployment (ECE)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
text: 'Node #start_linkmyNodeName#end_link is reporting disk usage of 91% at #absolute',
|
||||
tokens: [
|
||||
{
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
startToken: '#absolute',
|
||||
timestamp: 1,
|
||||
type: 'time',
|
||||
},
|
||||
{
|
||||
endToken: '#end_link',
|
||||
startToken: '#start_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes/myNodeId',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'myNodeId',
|
||||
context: {
|
||||
internalFullMessage: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify disk usage level of node.`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: 'Verify disk usage level of node.',
|
||||
clusterName,
|
||||
count,
|
||||
nodes: `${nodeName}:${diskUsage}`,
|
||||
node: `${nodeName}:${diskUsage}`,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify disk usage level of node.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchDiskUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat,
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new DiskUsageRule() as IDiskUsageAlertMock;
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'myNodeId',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: 'testCluster',
|
||||
cluster: {
|
||||
clusterName: 'testCluster',
|
||||
clusterUuid: 'abc123',
|
||||
},
|
||||
diskUsage: 91,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
ccs: 'testCluster',
|
||||
clusterUuid: 'abc123',
|
||||
diskUsage: 91,
|
||||
nodeId: 'myNodeId',
|
||||
nodeName: 'myNodeName',
|
||||
},
|
||||
nodeId: 'myNodeId',
|
||||
nodeName: 'myNodeName',
|
||||
ui: {
|
||||
isFiring: true,
|
||||
lastCheckedMS: 0,
|
||||
message: {
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkTune for disk usage#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/tune-for-disk-usage.html',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkIdentify large indices#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
startToken: '#start_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/indices',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkImplement ILM policies#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/index-lifecycle-management.html',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkAdd more data nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkResize your deployment (ECE)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
text: 'Node #start_linkmyNodeName#end_link is reporting disk usage of 91% at #absolute',
|
||||
tokens: [
|
||||
{
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
startToken: '#absolute',
|
||||
timestamp: 1,
|
||||
type: 'time',
|
||||
},
|
||||
{
|
||||
endToken: '#end_link',
|
||||
startToken: '#start_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes/myNodeId',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'myNodeId',
|
||||
context: {
|
||||
internalFullMessage: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`,
|
||||
internalShortMessage: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify disk usage level of node.`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/myNodeId?_g=(cluster_uuid:abc123,ccs:testCluster))`,
|
||||
actionPlain: 'Verify disk usage level of node.',
|
||||
clusterName,
|
||||
count,
|
||||
nodes: `${nodeName}:${diskUsage}`,
|
||||
node: `${nodeName}:${diskUsage}`,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify disk usage level of node.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,8 +8,10 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { Alert } from '@kbn/alerting-plugin/server';
|
||||
import { RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { AlertInstanceContext, RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { BaseRule } from './base_rule';
|
||||
import {
|
||||
AlertData,
|
||||
|
@ -151,7 +153,13 @@ export class DiskUsageRule extends BaseRule {
|
|||
}
|
||||
|
||||
protected executeActions(
|
||||
instance: Alert,
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
alertId: string,
|
||||
{ alertStates }: AlertInstanceState,
|
||||
item: AlertData | null,
|
||||
cluster: AlertCluster
|
||||
|
@ -200,19 +208,25 @@ export class DiskUsageRule extends BaseRule {
|
|||
}
|
||||
);
|
||||
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage,
|
||||
internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
/* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544
|
||||
services.alertsClient?.setAlertData({
|
||||
id: alertId,
|
||||
context: {
|
||||
internalShortMessage,
|
||||
internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
/* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544
|
||||
see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431
|
||||
*/
|
||||
nodes: `${firingNode.nodeName}:${firingNode.diskUsage}`,
|
||||
count: 1,
|
||||
node: `${firingNode.nodeName}:${firingNode.diskUsage}`,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
nodes: `${firingNode.nodeName}:${firingNode.diskUsage}`,
|
||||
count: 1,
|
||||
node: `${firingNode.nodeName}:${firingNode.diskUsage}`,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: internalShortMessage,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -9,7 +9,8 @@ import { ElasticsearchVersionMismatchRule } from './elasticsearch_version_mismat
|
|||
import { RULE_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants';
|
||||
import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
|
@ -79,24 +80,8 @@ describe('ElasticsearchVersionMismatchAlert', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
alertFactory: {
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
const services = alertsMock.createRuleExecutorServices();
|
||||
const executorOptions = { services, state: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
|
@ -111,52 +96,63 @@ describe('ElasticsearchVersionMismatchAlert', () => {
|
|||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fire actions', async () => {
|
||||
it('should fire action', async () => {
|
||||
const rule = new ElasticsearchVersionMismatchRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' },
|
||||
ccs,
|
||||
itemLabel: undefined,
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
meta: {
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' },
|
||||
ccs,
|
||||
clusterUuid,
|
||||
versions: ['8.0.0', '7.2.1'],
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'Multiple versions of Elasticsearch (8.0.0, 7.2.1) running in this cluster.',
|
||||
itemLabel: undefined,
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
meta: {
|
||||
ccs,
|
||||
clusterUuid,
|
||||
versions: ['8.0.0', '7.2.1'],
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'Multiple versions of Elasticsearch (8.0.0, 7.2.1) running in this cluster.',
|
||||
},
|
||||
severity: 'warning',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
severity: 'warning',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
action: `[View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: 'Verify you have the same version across all nodes.',
|
||||
internalFullMessage: `Elasticsearch version mismatch alert is firing for testCluster. Elasticsearch is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage:
|
||||
'Elasticsearch version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.',
|
||||
versionList: ['8.0.0', '7.2.1'],
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
context: {
|
||||
action: `[View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: 'Verify you have the same version across all nodes.',
|
||||
internalFullMessage: `Elasticsearch version mismatch alert is firing for testCluster. Elasticsearch is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage:
|
||||
'Elasticsearch version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.',
|
||||
versionList: ['8.0.0', '7.2.1'],
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]:
|
||||
'Elasticsearch version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -176,8 +172,8 @@ describe('ElasticsearchVersionMismatchAlert', () => {
|
|||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).not.toHaveBeenCalledWith({});
|
||||
expect(scheduleActions).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.report).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.setAlertData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { Alert } from '@kbn/alerting-plugin/server';
|
||||
import { SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { AlertInstanceContext, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { BaseRule } from './base_rule';
|
||||
import {
|
||||
AlertData,
|
||||
|
@ -87,7 +89,13 @@ export class ElasticsearchVersionMismatchRule extends BaseRule {
|
|||
}
|
||||
|
||||
protected async executeActions(
|
||||
instance: Alert,
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
alertId: string,
|
||||
{ alertStates }: AlertInstanceState,
|
||||
item: AlertData | null,
|
||||
cluster: AlertCluster
|
||||
|
@ -118,33 +126,40 @@ export class ElasticsearchVersionMismatchRule extends BaseRule {
|
|||
state.ccs
|
||||
);
|
||||
const action = `[${fullActionText}](${globalStateLink})`;
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage',
|
||||
{
|
||||
defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. {shortActionText}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
shortActionText,
|
||||
},
|
||||
}
|
||||
),
|
||||
internalFullMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage',
|
||||
{
|
||||
defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. Elasticsearch is running {versions}. {action}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
versions: versions.join(', '),
|
||||
action,
|
||||
},
|
||||
}
|
||||
),
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
clusterName: cluster.clusterName,
|
||||
versionList: versions,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
const internalShortMessage = i18n.translate(
|
||||
'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage',
|
||||
{
|
||||
defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. {shortActionText}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
shortActionText,
|
||||
},
|
||||
}
|
||||
);
|
||||
services.alertsClient?.setAlertData({
|
||||
id: alertId,
|
||||
context: {
|
||||
internalShortMessage,
|
||||
internalFullMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage',
|
||||
{
|
||||
defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. Elasticsearch is running {versions}. {action}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
versions: versions.join(', '),
|
||||
action,
|
||||
},
|
||||
}
|
||||
),
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
clusterName: cluster.clusterName,
|
||||
versionList: versions,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: internalShortMessage,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -20,4 +20,4 @@ export { NodesChangedRule } from './nodes_changed_rule';
|
|||
export { ElasticsearchVersionMismatchRule } from './elasticsearch_version_mismatch_rule';
|
||||
export { KibanaVersionMismatchRule } from './kibana_version_mismatch_rule';
|
||||
export { LogstashVersionMismatchRule } from './logstash_version_mismatch_rule';
|
||||
export { AlertsFactory } from './alerts_factory';
|
||||
export { RulesFactory } from './rules_factory';
|
|
@ -9,7 +9,8 @@ import { KibanaVersionMismatchRule } from './kibana_version_mismatch_rule';
|
|||
import { RULE_KIBANA_VERSION_MISMATCH } from '../../common/constants';
|
||||
import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
|
@ -82,24 +83,8 @@ describe('KibanaVersionMismatchRule', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
alertFactory: {
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
const services = alertsMock.createRuleExecutorServices();
|
||||
const executorOptions = { services, state: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
|
@ -114,52 +99,63 @@ describe('KibanaVersionMismatchRule', () => {
|
|||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fire actions', async () => {
|
||||
it('should fire action', async () => {
|
||||
const rule = new KibanaVersionMismatchRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' },
|
||||
ccs,
|
||||
itemLabel: undefined,
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
meta: {
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' },
|
||||
ccs,
|
||||
clusterUuid,
|
||||
versions: ['8.0.0', '7.2.1'],
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'Multiple versions of Kibana (8.0.0, 7.2.1) running in this cluster.',
|
||||
itemLabel: undefined,
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
meta: {
|
||||
ccs,
|
||||
clusterUuid,
|
||||
versions: ['8.0.0', '7.2.1'],
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'Multiple versions of Kibana (8.0.0, 7.2.1) running in this cluster.',
|
||||
},
|
||||
severity: 'warning',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
severity: 'warning',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
action: `[View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: 'Verify you have the same version across all instances.',
|
||||
internalFullMessage: `Kibana version mismatch alert is firing for testCluster. Kibana is running 8.0.0, 7.2.1. [View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage:
|
||||
'Kibana version mismatch alert is firing for testCluster. Verify you have the same version across all instances.',
|
||||
versionList: ['8.0.0', '7.2.1'],
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
context: {
|
||||
action: `[View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: 'Verify you have the same version across all instances.',
|
||||
internalFullMessage: `Kibana version mismatch alert is firing for testCluster. Kibana is running 8.0.0, 7.2.1. [View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage:
|
||||
'Kibana version mismatch alert is firing for testCluster. Verify you have the same version across all instances.',
|
||||
versionList: ['8.0.0', '7.2.1'],
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]:
|
||||
'Kibana version mismatch alert is firing for testCluster. Verify you have the same version across all instances.',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -179,8 +175,8 @@ describe('KibanaVersionMismatchRule', () => {
|
|||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).not.toHaveBeenCalledWith({});
|
||||
expect(scheduleActions).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.report).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.setAlertData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { Alert } from '@kbn/alerting-plugin/server';
|
||||
import { SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { AlertInstanceContext, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { BaseRule } from './base_rule';
|
||||
import {
|
||||
AlertData,
|
||||
|
@ -97,7 +99,13 @@ export class KibanaVersionMismatchRule extends BaseRule {
|
|||
}
|
||||
|
||||
protected async executeActions(
|
||||
instance: Alert,
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
alertId: string,
|
||||
{ alertStates }: AlertInstanceState,
|
||||
item: AlertData | null,
|
||||
cluster: AlertCluster
|
||||
|
@ -128,6 +136,16 @@ export class KibanaVersionMismatchRule extends BaseRule {
|
|||
state.ccs
|
||||
);
|
||||
const action = `[${fullActionText}](${globalStateLink})`;
|
||||
const internalShortMessage = i18n.translate(
|
||||
'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage',
|
||||
{
|
||||
defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
shortActionText,
|
||||
},
|
||||
}
|
||||
);
|
||||
const internalFullMessage = i18n.translate(
|
||||
'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalFullMessage',
|
||||
{
|
||||
|
@ -139,23 +157,20 @@ export class KibanaVersionMismatchRule extends BaseRule {
|
|||
},
|
||||
}
|
||||
);
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage',
|
||||
{
|
||||
defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
shortActionText,
|
||||
},
|
||||
}
|
||||
),
|
||||
internalFullMessage,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
clusterName: cluster.clusterName,
|
||||
versionList: versions,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
services.alertsClient?.setAlertData({
|
||||
id: alertId,
|
||||
context: {
|
||||
internalShortMessage,
|
||||
internalFullMessage,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
clusterName: cluster.clusterName,
|
||||
versionList: versions,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: internalShortMessage,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,345 @@
|
|||
/*
|
||||
* 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 { LargeShardSizeRule } from './large_shard_size_rule';
|
||||
import { RULE_LARGE_SHARD_SIZE } from '../../common/constants';
|
||||
import { fetchIndexShardSize } from '../lib/alerts/fetch_index_shard_size';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
|
||||
type ILargeShardSizeRuleMock = LargeShardSizeRule & {
|
||||
defaultParams: {
|
||||
threshold: number;
|
||||
duration: string;
|
||||
};
|
||||
} & {
|
||||
actionVariables: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_index_shard_size', () => ({
|
||||
fetchIndexShardSize: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('LargeShardSizeRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock;
|
||||
expect(rule.ruleOptions.id).toBe(RULE_LARGE_SHARD_SIZE);
|
||||
expect(rule.ruleOptions.name).toBe('Shard size');
|
||||
expect(rule.ruleOptions.throttle).toBe('12h');
|
||||
expect(rule.ruleOptions.defaultParams).toStrictEqual({
|
||||
threshold: 55,
|
||||
indexPattern: '-.*',
|
||||
});
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'shardIndex', description: 'The index experiencing large average shard size.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
describe('execute', () => {
|
||||
const FakeDate = function () {};
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const shardIndex = 'apm-8.0.0-onboarding-2021.06.30';
|
||||
const shardSize = 0;
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const stat = {
|
||||
shardIndex,
|
||||
shardSize,
|
||||
clusterUuid,
|
||||
};
|
||||
|
||||
const services = alertsMock.createRuleExecutorServices();
|
||||
const executorOptions = { services, state: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
Date = FakeDate as DateConstructor;
|
||||
(fetchIndexShardSize as jest.Mock).mockImplementation(() => {
|
||||
return [stat];
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fire action', async () => {
|
||||
const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock;
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'abc123:apm-8.0.0-onboarding-2021.06.30',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: undefined,
|
||||
cluster: {
|
||||
clusterName: 'testCluster',
|
||||
clusterUuid: 'abc123',
|
||||
},
|
||||
itemLabel: 'apm-8.0.0-onboarding-2021.06.30',
|
||||
meta: {
|
||||
instanceId: 'abc123:apm-8.0.0-onboarding-2021.06.30',
|
||||
itemLabel: 'apm-8.0.0-onboarding-2021.06.30',
|
||||
shardIndex: 'apm-8.0.0-onboarding-2021.06.30',
|
||||
shardSize: 0,
|
||||
},
|
||||
nodeId: 'abc123:apm-8.0.0-onboarding-2021.06.30',
|
||||
nodeName: 'apm-8.0.0-onboarding-2021.06.30',
|
||||
ui: {
|
||||
isFiring: true,
|
||||
lastCheckedMS: 0,
|
||||
message: {
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkInvestigate detailed index stats#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
startToken: '#start_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/indices/apm-8.0.0-onboarding-2021.06.30/advanced',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkHow to size your shards (Docs)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/current/size-your-shards.html',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkShard sizing tips (Blog)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
text: 'The following index: #start_linkapm-8.0.0-onboarding-2021.06.30#end_link has a large average shard size of: 0GB at #absolute',
|
||||
tokens: [
|
||||
{
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
startToken: '#absolute',
|
||||
timestamp: 1,
|
||||
type: 'time',
|
||||
},
|
||||
{
|
||||
endToken: '#end_link',
|
||||
startToken: '#start_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/indices/apm-8.0.0-onboarding-2021.06.30',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'abc123:apm-8.0.0-onboarding-2021.06.30',
|
||||
context: {
|
||||
internalFullMessage: `Large shard size alert is firing for the following index: ${shardIndex}. [View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `Large shard size alert is firing for the following index: ${shardIndex}. Investigate indices with large shard sizes.`,
|
||||
action: `[View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: 'Investigate indices with large shard sizes.',
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
shardIndex,
|
||||
shardIndices: shardIndex,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `Large shard size alert is firing for the following index: ${shardIndex}. Investigate indices with large shard sizes.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchIndexShardSize as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat,
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock;
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'abc123:apm-8.0.0-onboarding-2021.06.30',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: 'testCluster',
|
||||
cluster: {
|
||||
clusterName: 'testCluster',
|
||||
clusterUuid: 'abc123',
|
||||
},
|
||||
itemLabel: 'apm-8.0.0-onboarding-2021.06.30',
|
||||
meta: {
|
||||
instanceId: 'abc123:apm-8.0.0-onboarding-2021.06.30',
|
||||
itemLabel: 'apm-8.0.0-onboarding-2021.06.30',
|
||||
shardIndex: 'apm-8.0.0-onboarding-2021.06.30',
|
||||
shardSize: 0,
|
||||
},
|
||||
nodeId: 'abc123:apm-8.0.0-onboarding-2021.06.30',
|
||||
nodeName: 'apm-8.0.0-onboarding-2021.06.30',
|
||||
ui: {
|
||||
isFiring: true,
|
||||
lastCheckedMS: 0,
|
||||
message: {
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkInvestigate detailed index stats#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
startToken: '#start_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/indices/apm-8.0.0-onboarding-2021.06.30/advanced',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkHow to size your shards (Docs)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/current/size-your-shards.html',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkShard sizing tips (Blog)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
endToken: '#end_link',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster',
|
||||
startToken: '#start_link',
|
||||
type: 'docLink',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
text: 'The following index: #start_linkapm-8.0.0-onboarding-2021.06.30#end_link has a large average shard size of: 0GB at #absolute',
|
||||
tokens: [
|
||||
{
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
startToken: '#absolute',
|
||||
timestamp: 1,
|
||||
type: 'time',
|
||||
},
|
||||
{
|
||||
endToken: '#end_link',
|
||||
startToken: '#start_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/indices/apm-8.0.0-onboarding-2021.06.30',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'abc123:apm-8.0.0-onboarding-2021.06.30',
|
||||
context: {
|
||||
internalFullMessage: `Large shard size alert is firing for the following index: ${shardIndex}. [View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`,
|
||||
internalShortMessage: `Large shard size alert is firing for the following index: ${shardIndex}. Investigate indices with large shard sizes.`,
|
||||
action: `[View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`,
|
||||
actionPlain: 'Investigate indices with large shard sizes.',
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
shardIndex,
|
||||
shardIndices: shardIndex,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `Large shard size alert is firing for the following index: ${shardIndex}. Investigate indices with large shard sizes.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { Alert } from '@kbn/alerting-plugin/server';
|
||||
import { SanitizedRule, RawAlertInstance } from '@kbn/alerting-plugin/common';
|
||||
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { SanitizedRule, RawAlertInstance, AlertInstanceContext } from '@kbn/alerting-plugin/common';
|
||||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { BaseRule } from './base_rule';
|
||||
import {
|
||||
AlertData,
|
||||
|
@ -149,7 +151,13 @@ export class LargeShardSizeRule extends BaseRule {
|
|||
}
|
||||
|
||||
protected executeActions(
|
||||
instance: Alert,
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
alertId: string,
|
||||
{ alertStates }: AlertInstanceState,
|
||||
item: AlertData | null,
|
||||
cluster: AlertCluster
|
||||
|
@ -196,19 +204,25 @@ export class LargeShardSizeRule extends BaseRule {
|
|||
}
|
||||
);
|
||||
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage,
|
||||
internalFullMessage,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
/* continue to send "shardIndices" values for users still using it though
|
||||
services.alertsClient?.setAlertData({
|
||||
id: alertId,
|
||||
context: {
|
||||
internalShortMessage,
|
||||
internalFullMessage,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
/* continue to send "shardIndices" values for users still using it though
|
||||
we have replaced it with shardIndex in the template due to alerts per index instead of all indices
|
||||
see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431
|
||||
*/
|
||||
shardIndices: shardIndex,
|
||||
shardIndex,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
shardIndices: shardIndex,
|
||||
shardIndex,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: internalShortMessage,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,387 @@
|
|||
/*
|
||||
* 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 { LicenseExpirationRule } from './license_expiration_rule';
|
||||
import { RULE_LICENSE_EXPIRATION } from '../../common/constants';
|
||||
import { fetchLicenses } from '../lib/alerts/fetch_licenses';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_licenses', () => ({
|
||||
fetchLicenses: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
config: {
|
||||
ui: {
|
||||
show_license_expiration: true,
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('LicenseExpirationRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new LicenseExpirationRule();
|
||||
expect(rule.ruleOptions.id).toBe(RULE_LICENSE_EXPIRATION);
|
||||
expect(rule.ruleOptions.name).toBe('License expiration');
|
||||
expect(rule.ruleOptions.throttle).toBe('1d');
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'expiredDate', description: 'The date when the license expires.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the license belong.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
function FakeDate() {}
|
||||
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const license = {
|
||||
status: 'expired',
|
||||
type: 'gold',
|
||||
expiryDateMS: 1000 * 60 * 60 * 24 * 59,
|
||||
clusterUuid,
|
||||
};
|
||||
|
||||
const services = alertsMock.createRuleExecutorServices();
|
||||
const executorOptions = { services, state: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
Date = FakeDate;
|
||||
(fetchLicenses as jest.Mock).mockImplementation(() => {
|
||||
return [license];
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should fire action', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-03-30T00:00:00.000Z'));
|
||||
const alert = new LicenseExpirationRule();
|
||||
const type = alert.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: alert.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid, clusterName },
|
||||
ccs: undefined,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
clusterUuid: 'abc123',
|
||||
expiryDateMS: 5097600000,
|
||||
status: 'expired',
|
||||
type: 'gold',
|
||||
},
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#relative',
|
||||
type: 'time',
|
||||
isRelative: true,
|
||||
isAbsolute: false,
|
||||
timestamp: 5097600000,
|
||||
},
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 5097600000,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'license',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1680134400000,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
context: {
|
||||
action: '[Please update your license.](elasticsearch/nodes)',
|
||||
actionPlain: 'Please update your license.',
|
||||
internalFullMessage:
|
||||
'License expiration alert is firing for testCluster. Your license expires in 53 years. [Please update your license.](elasticsearch/nodes)',
|
||||
internalShortMessage:
|
||||
'License expiration alert is firing for testCluster. Your license expires in 53 years. Please update your license.',
|
||||
clusterName,
|
||||
expiredDate: '53 years',
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]:
|
||||
'License expiration alert is firing for testCluster. Your license expires in 53 years. Please update your license.',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fire actions if the license is not expired', async () => {
|
||||
(fetchLicenses as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
status: 'active',
|
||||
type: 'gold',
|
||||
expiryDateMS: 1000 * 60 * 60 * 24 * 61,
|
||||
clusterUuid,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new LicenseExpirationRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.setAlertData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use danger severity for a license expiring soon', async () => {
|
||||
(fetchLicenses as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
status: 'active',
|
||||
type: 'gold',
|
||||
expiryDateMS: 1000 * 60 * 60 * 24 * 2,
|
||||
clusterUuid,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new LicenseExpirationRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid, clusterName },
|
||||
ccs: undefined,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
clusterUuid: 'abc123',
|
||||
expiryDateMS: 172800000,
|
||||
status: 'active',
|
||||
type: 'gold',
|
||||
},
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#relative',
|
||||
type: 'time',
|
||||
isRelative: true,
|
||||
isAbsolute: false,
|
||||
timestamp: 172800000,
|
||||
},
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 172800000,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'license',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
context: {
|
||||
action: '[Please update your license.](elasticsearch/nodes)',
|
||||
actionPlain: 'Please update your license.',
|
||||
internalFullMessage:
|
||||
'License expiration alert is firing for testCluster. Your license expires in 2 days. [Please update your license.](elasticsearch/nodes)',
|
||||
internalShortMessage:
|
||||
'License expiration alert is firing for testCluster. Your license expires in 2 days. Please update your license.',
|
||||
clusterName,
|
||||
expiredDate: '2 days',
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]:
|
||||
'License expiration alert is firing for testCluster. Your license expires in 2 days. Please update your license.',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should use warning severity for a license expiring in a bit', async () => {
|
||||
(fetchLicenses as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
status: 'active',
|
||||
type: 'gold',
|
||||
expiryDateMS: 1000 * 60 * 60 * 24 * 31,
|
||||
clusterUuid,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new LicenseExpirationRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid, clusterName },
|
||||
ccs: undefined,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
clusterUuid: 'abc123',
|
||||
expiryDateMS: 2678400000,
|
||||
status: 'active',
|
||||
type: 'gold',
|
||||
},
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#relative',
|
||||
type: 'time',
|
||||
isRelative: true,
|
||||
isAbsolute: false,
|
||||
timestamp: 2678400000,
|
||||
},
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 2678400000,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'license',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'warning',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
context: {
|
||||
action: '[Please update your license.](elasticsearch/nodes)',
|
||||
actionPlain: 'Please update your license.',
|
||||
internalFullMessage:
|
||||
'License expiration alert is firing for testCluster. Your license expires in a month. [Please update your license.](elasticsearch/nodes)',
|
||||
internalShortMessage:
|
||||
'License expiration alert is firing for testCluster. Your license expires in a month. Please update your license.',
|
||||
clusterName,
|
||||
expiredDate: 'a month',
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]:
|
||||
'License expiration alert is firing for testCluster. Your license expires in a month. Please update your license.',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,8 +7,10 @@
|
|||
import moment from 'moment';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { RuleExecutorOptions, Alert } from '@kbn/alerting-plugin/server';
|
||||
import { SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { RuleExecutorOptions, RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { AlertInstanceContext, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { BaseRule } from './base_rule';
|
||||
import {
|
||||
AlertData,
|
||||
|
@ -143,7 +145,13 @@ export class LicenseExpirationRule extends BaseRule {
|
|||
}
|
||||
|
||||
protected async executeActions(
|
||||
instance: Alert,
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
alertId: string,
|
||||
{ alertStates }: AlertInstanceState,
|
||||
item: AlertData | null,
|
||||
cluster: AlertCluster
|
||||
|
@ -161,34 +169,41 @@ export class LicenseExpirationRule extends BaseRule {
|
|||
});
|
||||
const action = `[${actionText}](elasticsearch/nodes)`;
|
||||
const expiredDate = $duration.humanize();
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage',
|
||||
{
|
||||
defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {actionText}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
expiredDate,
|
||||
actionText,
|
||||
},
|
||||
}
|
||||
),
|
||||
internalFullMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage',
|
||||
{
|
||||
defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {action}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
expiredDate,
|
||||
action,
|
||||
},
|
||||
}
|
||||
),
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
expiredDate,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: actionText,
|
||||
const internalShortMessage = i18n.translate(
|
||||
'xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage',
|
||||
{
|
||||
defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {actionText}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
expiredDate,
|
||||
actionText,
|
||||
},
|
||||
}
|
||||
);
|
||||
services.alertsClient?.setAlertData({
|
||||
id: alertId,
|
||||
context: {
|
||||
internalShortMessage,
|
||||
internalFullMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage',
|
||||
{
|
||||
defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {action}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
expiredDate,
|
||||
action,
|
||||
},
|
||||
}
|
||||
),
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
expiredDate,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: actionText,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: internalShortMessage,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -9,7 +9,8 @@ import { LogstashVersionMismatchRule } from './logstash_version_mismatch_rule';
|
|||
import { RULE_LOGSTASH_VERSION_MISMATCH } from '../../common/constants';
|
||||
import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
|
@ -80,24 +81,8 @@ describe('LogstashVersionMismatchRule', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
alertFactory: {
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
const services = alertsMock.createRuleExecutorServices();
|
||||
const executorOptions = { services, state: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
|
@ -112,52 +97,63 @@ describe('LogstashVersionMismatchRule', () => {
|
|||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fire actions', async () => {
|
||||
it('should fire action', async () => {
|
||||
const rule = new LogstashVersionMismatchRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' },
|
||||
ccs,
|
||||
itemLabel: undefined,
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
meta: {
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' },
|
||||
ccs,
|
||||
clusterUuid,
|
||||
versions: ['8.0.0', '7.2.1'],
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'Multiple versions of Logstash (8.0.0, 7.2.1) running in this cluster.',
|
||||
itemLabel: undefined,
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
meta: {
|
||||
ccs,
|
||||
clusterUuid,
|
||||
versions: ['8.0.0', '7.2.1'],
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'Multiple versions of Logstash (8.0.0, 7.2.1) running in this cluster.',
|
||||
},
|
||||
severity: 'warning',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
severity: 'warning',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
action: `[View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: 'Verify you have the same version across all nodes.',
|
||||
internalFullMessage: `Logstash version mismatch alert is firing for testCluster. Logstash is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage:
|
||||
'Logstash version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.',
|
||||
versionList: ['8.0.0', '7.2.1'],
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
context: {
|
||||
action: `[View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: 'Verify you have the same version across all nodes.',
|
||||
internalFullMessage: `Logstash version mismatch alert is firing for testCluster. Logstash is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage:
|
||||
'Logstash version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.',
|
||||
versionList: ['8.0.0', '7.2.1'],
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]:
|
||||
'Logstash version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -177,8 +173,8 @@ describe('LogstashVersionMismatchRule', () => {
|
|||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).not.toHaveBeenCalledWith({});
|
||||
expect(scheduleActions).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.report).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.setAlertData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { Alert } from '@kbn/alerting-plugin/server';
|
||||
import { SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { AlertInstanceContext, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { BaseRule } from './base_rule';
|
||||
import {
|
||||
AlertData,
|
||||
|
@ -87,7 +89,13 @@ export class LogstashVersionMismatchRule extends BaseRule {
|
|||
}
|
||||
|
||||
protected async executeActions(
|
||||
instance: Alert,
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
alertId: string,
|
||||
{ alertStates }: AlertInstanceState,
|
||||
item: AlertData | null,
|
||||
cluster: AlertCluster
|
||||
|
@ -118,33 +126,40 @@ export class LogstashVersionMismatchRule extends BaseRule {
|
|||
state.ccs
|
||||
);
|
||||
const action = `[${fullActionText}](${globalStateLink})`;
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage',
|
||||
{
|
||||
defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. {shortActionText}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
shortActionText,
|
||||
},
|
||||
}
|
||||
),
|
||||
internalFullMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage',
|
||||
{
|
||||
defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. Logstash is running {versions}. {action}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
versions: versions.join(', '),
|
||||
action,
|
||||
},
|
||||
}
|
||||
),
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
clusterName: cluster.clusterName,
|
||||
versionList: versions,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
const internalShortMessage = i18n.translate(
|
||||
'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage',
|
||||
{
|
||||
defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. {shortActionText}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
shortActionText,
|
||||
},
|
||||
}
|
||||
);
|
||||
services.alertsClient?.setAlertData({
|
||||
id: alertId,
|
||||
context: {
|
||||
internalShortMessage,
|
||||
internalFullMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage',
|
||||
{
|
||||
defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. Logstash is running {versions}. {action}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
versions: versions.join(', '),
|
||||
action,
|
||||
},
|
||||
}
|
||||
),
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
clusterName: cluster.clusterName,
|
||||
versionList: versions,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: internalShortMessage,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
399
x-pack/plugins/monitoring/server/rules/memory_usage_rule.test.ts
Normal file
399
x-pack/plugins/monitoring/server/rules/memory_usage_rule.test.ts
Normal file
|
@ -0,0 +1,399 @@
|
|||
/*
|
||||
* 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 { MemoryUsageRule } from './memory_usage_rule';
|
||||
import { RULE_MEMORY_USAGE } from '../../common/constants';
|
||||
import { fetchMemoryUsageNodeStats } from '../lib/alerts/fetch_memory_usage_node_stats';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_memory_usage_node_stats', () => ({
|
||||
fetchMemoryUsageNodeStats: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('MemoryUsageRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new MemoryUsageRule();
|
||||
expect(rule.ruleOptions.id).toBe(RULE_MEMORY_USAGE);
|
||||
expect(rule.ruleOptions.name).toBe('Memory Usage (JVM)');
|
||||
expect(rule.ruleOptions.throttle).toBe('1d');
|
||||
expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' });
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'node', description: 'The node reporting high memory usage.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
function FakeDate() {}
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const nodeId = 'myNodeId';
|
||||
const nodeName = 'myNodeName';
|
||||
const memoryUsage = 91;
|
||||
const stat = {
|
||||
clusterUuid,
|
||||
nodeId,
|
||||
nodeName,
|
||||
memoryUsage,
|
||||
};
|
||||
|
||||
const services = alertsMock.createRuleExecutorServices();
|
||||
const executorOptions = { services, state: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
Date = FakeDate;
|
||||
(fetchMemoryUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [stat];
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fire action', async () => {
|
||||
const rule = new MemoryUsageRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'myNodeId',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: undefined,
|
||||
cluster: { clusterUuid, clusterName },
|
||||
memoryUsage,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
clusterUuid,
|
||||
memoryUsage,
|
||||
nodeId,
|
||||
nodeName,
|
||||
},
|
||||
nodeId,
|
||||
nodeName,
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: `Node #start_link${nodeName}#end_link is reporting JVM memory usage of ${memoryUsage}% at #absolute`,
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkTune thread pools#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkManaging ES Heap#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl: '{elasticWebsiteUrl}blog/a-heap-of-trouble',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkIdentify large indices/shards#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/indices',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkAdd more data nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkResize your deployment (ECE)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes/myNodeId',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'myNodeId',
|
||||
context: {
|
||||
internalFullMessage: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify memory usage level of node.`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: 'Verify memory usage level of node.',
|
||||
clusterName,
|
||||
count,
|
||||
nodes: `${nodeName}:${memoryUsage}.00`,
|
||||
node: `${nodeName}:${memoryUsage}.00`,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify memory usage level of node.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fire actions if under threshold', async () => {
|
||||
(fetchMemoryUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat,
|
||||
memoryUsage: 1,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new MemoryUsageRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.setAlertData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchMemoryUsageNodeStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat,
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new MemoryUsageRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'myNodeId',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: 'testCluster',
|
||||
cluster: { clusterUuid, clusterName },
|
||||
memoryUsage,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
ccs: 'testCluster',
|
||||
clusterUuid,
|
||||
memoryUsage,
|
||||
nodeId,
|
||||
nodeName,
|
||||
},
|
||||
nodeId,
|
||||
nodeName,
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: `Node #start_link${nodeName}#end_link is reporting JVM memory usage of ${memoryUsage}% at #absolute`,
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkTune thread pools#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkManaging ES Heap#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl: '{elasticWebsiteUrl}blog/a-heap-of-trouble',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkIdentify large indices/shards#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/indices',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkAdd more data nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkResize your deployment (ECE)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes/myNodeId',
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'myNodeId',
|
||||
context: {
|
||||
internalFullMessage: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`,
|
||||
internalShortMessage: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify memory usage level of node.`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`,
|
||||
actionPlain: 'Verify memory usage level of node.',
|
||||
clusterName,
|
||||
count,
|
||||
nodes: `${nodeName}:${memoryUsage}.00`,
|
||||
node: `${nodeName}:${memoryUsage}.00`,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify memory usage level of node.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,9 +8,11 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { Alert } from '@kbn/alerting-plugin/server';
|
||||
import { RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { AlertInstanceContext, RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import { parseDuration } from '@kbn/alerting-plugin/common/parse_duration';
|
||||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { BaseRule } from './base_rule';
|
||||
import {
|
||||
AlertData,
|
||||
|
@ -157,7 +159,13 @@ export class MemoryUsageRule extends BaseRule {
|
|||
}
|
||||
|
||||
protected executeActions(
|
||||
instance: Alert,
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
alertId: string,
|
||||
{ alertStates }: AlertInstanceState,
|
||||
item: AlertData | null,
|
||||
cluster: AlertCluster
|
||||
|
@ -207,19 +215,25 @@ export class MemoryUsageRule extends BaseRule {
|
|||
}
|
||||
);
|
||||
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage,
|
||||
internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
/* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544
|
||||
services.alertsClient?.setAlertData({
|
||||
id: alertId,
|
||||
context: {
|
||||
internalShortMessage,
|
||||
internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
/* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544
|
||||
see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431
|
||||
*/
|
||||
nodes: `${firingNode.nodeName}:${firingNode.memoryUsage.toFixed(2)}`,
|
||||
count: 1,
|
||||
node: `${firingNode.nodeName}:${firingNode.memoryUsage.toFixed(2)}`,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
nodes: `${firingNode.nodeName}:${firingNode.memoryUsage.toFixed(2)}`,
|
||||
count: 1,
|
||||
node: `${firingNode.nodeName}:${firingNode.memoryUsage.toFixed(2)}`,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: internalShortMessage,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,307 @@
|
|||
/*
|
||||
* 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 { MissingMonitoringDataRule } from './missing_monitoring_data_rule';
|
||||
import { RULE_MISSING_MONITORING_DATA } from '../../common/constants';
|
||||
import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_missing_monitoring_data', () => ({
|
||||
fetchMissingMonitoringData: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
show_license_expiration: true,
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('MissingMonitoringDataRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new MissingMonitoringDataRule();
|
||||
expect(rule.ruleOptions.id).toBe(RULE_MISSING_MONITORING_DATA);
|
||||
expect(rule.ruleOptions.name).toBe('Missing monitoring data');
|
||||
expect(rule.ruleOptions.throttle).toBe('6h');
|
||||
expect(rule.ruleOptions.defaultParams).toStrictEqual({ limit: '1d', duration: '15m' });
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'node', description: 'The node missing monitoring data.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
function FakeDate() {}
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const nodeId = 'esNode1';
|
||||
const nodeName = 'esName1';
|
||||
const gapDuration = 3000001;
|
||||
const missingData = [
|
||||
{
|
||||
nodeId,
|
||||
nodeName,
|
||||
clusterUuid,
|
||||
gapDuration,
|
||||
},
|
||||
];
|
||||
|
||||
const services = alertsMock.createRuleExecutorServices();
|
||||
const executorOptions = { services, state: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
Date = FakeDate;
|
||||
(fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
|
||||
return missingData;
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fire action', async () => {
|
||||
const rule = new MissingMonitoringDataRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'esNode1',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: undefined,
|
||||
cluster: { clusterUuid, clusterName },
|
||||
nodeId,
|
||||
nodeName,
|
||||
gapDuration,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
clusterUuid,
|
||||
gapDuration,
|
||||
limit: 86400000,
|
||||
nodeId,
|
||||
nodeName,
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'For the past an hour, we have not detected any monitoring data from the Elasticsearch node: esName1, starting at #absolute',
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkView all Elasticsearch nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Verify monitoring settings on the node',
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'esNode1',
|
||||
context: {
|
||||
internalFullMessage: `We have not detected any monitoring data for node ${nodeName} in cluster: ${clusterName}. [View what monitoring data we do have for this node.](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `We have not detected any monitoring data for node ${nodeName} in cluster: ${clusterName}. Verify the node is up and running, then double check the monitoring settings.`,
|
||||
nodes: `node: ${nodeName}`,
|
||||
node: `node: ${nodeName}`,
|
||||
action: `[View what monitoring data we do have for this node.](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain:
|
||||
'Verify the node is up and running, then double check the monitoring settings.',
|
||||
clusterName,
|
||||
count,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `We have not detected any monitoring data for node ${nodeName} in cluster: ${clusterName}. Verify the node is up and running, then double check the monitoring settings.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fire actions if under threshold', async () => {
|
||||
(fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...missingData[0],
|
||||
gapDuration: 1,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new MissingMonitoringDataRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.setAlertData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...missingData[0],
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new MissingMonitoringDataRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'esNode1',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: 'testCluster',
|
||||
cluster: { clusterUuid, clusterName },
|
||||
nodeId,
|
||||
nodeName,
|
||||
gapDuration,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
ccs: 'testCluster',
|
||||
clusterUuid,
|
||||
gapDuration,
|
||||
limit: 86400000,
|
||||
nodeId,
|
||||
nodeName,
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: 'For the past an hour, we have not detected any monitoring data from the Elasticsearch node: esName1, starting at #absolute',
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkView all Elasticsearch nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Verify monitoring settings on the node',
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'esNode1',
|
||||
context: {
|
||||
internalFullMessage: `We have not detected any monitoring data for node ${nodeName} in cluster: ${clusterName}. [View what monitoring data we do have for this node.](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`,
|
||||
internalShortMessage: `We have not detected any monitoring data for node ${nodeName} in cluster: ${clusterName}. Verify the node is up and running, then double check the monitoring settings.`,
|
||||
nodes: `node: ${nodeName}`,
|
||||
node: `node: ${nodeName}`,
|
||||
action: `[View what monitoring data we do have for this node.](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`,
|
||||
actionPlain:
|
||||
'Verify the node is up and running, then double check the monitoring settings.',
|
||||
clusterName,
|
||||
count,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `We have not detected any monitoring data for node ${nodeName} in cluster: ${clusterName}. Verify the node is up and running, then double check the monitoring settings.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,9 +8,16 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { Alert } from '@kbn/alerting-plugin/server';
|
||||
import { RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
|
||||
import {
|
||||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
RawAlertInstance,
|
||||
SanitizedRule,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { parseDuration } from '@kbn/alerting-plugin/common/parse_duration';
|
||||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { BaseRule } from './base_rule';
|
||||
import {
|
||||
AlertData,
|
||||
|
@ -137,7 +144,13 @@ export class MissingMonitoringDataRule extends BaseRule {
|
|||
}
|
||||
|
||||
protected executeActions(
|
||||
instance: Alert,
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
alertId: string,
|
||||
{ alertStates }: { alertStates: AlertState[] },
|
||||
item: AlertData | null,
|
||||
cluster: AlertCluster
|
||||
|
@ -187,19 +200,25 @@ export class MissingMonitoringDataRule extends BaseRule {
|
|||
},
|
||||
}
|
||||
);
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage,
|
||||
internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
/* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544
|
||||
services.alertsClient?.setAlertData({
|
||||
id: alertId,
|
||||
context: {
|
||||
internalShortMessage,
|
||||
internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
/* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544
|
||||
see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431
|
||||
*/
|
||||
nodes: `node: ${firingNode.nodeName}`,
|
||||
count: 1,
|
||||
node: `node: ${firingNode.nodeName}`,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
nodes: `node: ${firingNode.nodeName}`,
|
||||
count: 1,
|
||||
node: `node: ${firingNode.nodeName}`,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: internalShortMessage,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -9,7 +9,8 @@ import { NodesChangedRule } from './nodes_changed_rule';
|
|||
import { RULE_NODES_CHANGED } from '../../common/constants';
|
||||
import { fetchNodesFromClusterStats } from '../lib/alerts/fetch_nodes_from_cluster_stats';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
|
@ -125,24 +126,8 @@ describe('NodesChangedAlert', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
alertFactory: {
|
||||
create: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
const services = alertsMock.createRuleExecutorServices();
|
||||
const executorOptions = { services, state: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
|
@ -154,12 +139,10 @@ describe('NodesChangedAlert', () => {
|
|||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fire actions when nodes change', async () => {
|
||||
it('should fire action when nodes change', async () => {
|
||||
(fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => {
|
||||
return nodesChanged;
|
||||
});
|
||||
|
@ -169,59 +152,72 @@ describe('NodesChangedAlert', () => {
|
|||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid, clusterName },
|
||||
ccs,
|
||||
itemLabel: undefined,
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
meta: {
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid, clusterName },
|
||||
ccs,
|
||||
clusterUuid,
|
||||
recentNodes: [
|
||||
{
|
||||
nodeUuid,
|
||||
nodeEphemeralId: nodeEphemeralIdChanged,
|
||||
nodeName,
|
||||
},
|
||||
],
|
||||
priorNodes: [
|
||||
{
|
||||
nodeUuid,
|
||||
nodeEphemeralId,
|
||||
nodeName,
|
||||
},
|
||||
],
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: "Elasticsearch nodes 'test' restarted in this cluster.",
|
||||
itemLabel: undefined,
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
meta: {
|
||||
ccs,
|
||||
clusterUuid,
|
||||
recentNodes: [
|
||||
{
|
||||
nodeUuid,
|
||||
nodeEphemeralId: nodeEphemeralIdChanged,
|
||||
nodeName,
|
||||
},
|
||||
],
|
||||
priorNodes: [
|
||||
{
|
||||
nodeUuid,
|
||||
nodeEphemeralId,
|
||||
nodeName,
|
||||
},
|
||||
],
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: "Elasticsearch nodes 'test' restarted in this cluster.",
|
||||
},
|
||||
severity: 'warning',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
severity: 'warning',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
action: '[View nodes](elasticsearch/nodes)',
|
||||
actionPlain: 'Verify that you added, removed, or restarted nodes.',
|
||||
internalFullMessage:
|
||||
'Nodes changed alert is firing for testCluster. The following Elasticsearch nodes have been added: removed: restarted:test. [View nodes](elasticsearch/nodes)',
|
||||
internalShortMessage:
|
||||
'Nodes changed alert is firing for testCluster. Verify that you added, removed, or restarted nodes.',
|
||||
added: '',
|
||||
removed: '',
|
||||
restarted: 'test',
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
context: {
|
||||
action: '[View nodes](elasticsearch/nodes)',
|
||||
actionPlain: 'Verify that you added, removed, or restarted nodes.',
|
||||
internalFullMessage:
|
||||
'Nodes changed alert is firing for testCluster. The following Elasticsearch nodes have been added: removed: restarted:test. [View nodes](elasticsearch/nodes)',
|
||||
internalShortMessage:
|
||||
'Nodes changed alert is firing for testCluster. Verify that you added, removed, or restarted nodes.',
|
||||
added: '',
|
||||
removed: '',
|
||||
restarted: 'test',
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]:
|
||||
'Nodes changed alert is firing for testCluster. Verify that you added, removed, or restarted nodes.',
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should fire actions when nodes added, changed, and removed', async () => {
|
||||
it('should fire action when nodes added, changed, and removed', async () => {
|
||||
(fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => {
|
||||
return nodesAddedChangedRemoved;
|
||||
});
|
||||
|
@ -231,66 +227,79 @@ describe('NodesChangedAlert', () => {
|
|||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid, clusterName },
|
||||
ccs,
|
||||
itemLabel: undefined,
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
meta: {
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid, clusterName },
|
||||
ccs,
|
||||
clusterUuid,
|
||||
recentNodes: [
|
||||
{
|
||||
nodeUuid,
|
||||
nodeEphemeralId: nodeEphemeralIdChanged,
|
||||
nodeName,
|
||||
},
|
||||
{
|
||||
nodeUuid: 'newNodeId',
|
||||
nodeEphemeralId: 'newNodeEmpheralId',
|
||||
nodeName: 'newNodeName',
|
||||
},
|
||||
],
|
||||
priorNodes: [
|
||||
{
|
||||
nodeUuid,
|
||||
nodeEphemeralId,
|
||||
nodeName,
|
||||
},
|
||||
{
|
||||
nodeUuid: 'removedNodeId',
|
||||
nodeEphemeralId: 'removedNodeEmpheralId',
|
||||
nodeName: 'removedNodeName',
|
||||
},
|
||||
],
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: "Elasticsearch nodes 'newNodeName' added to this cluster. Elasticsearch nodes 'removedNodeName' removed from this cluster. Elasticsearch nodes 'test' restarted in this cluster.",
|
||||
itemLabel: undefined,
|
||||
nodeId: undefined,
|
||||
nodeName: undefined,
|
||||
meta: {
|
||||
ccs,
|
||||
clusterUuid,
|
||||
recentNodes: [
|
||||
{
|
||||
nodeUuid,
|
||||
nodeEphemeralId: nodeEphemeralIdChanged,
|
||||
nodeName,
|
||||
},
|
||||
{
|
||||
nodeUuid: 'newNodeId',
|
||||
nodeEphemeralId: 'newNodeEmpheralId',
|
||||
nodeName: 'newNodeName',
|
||||
},
|
||||
],
|
||||
priorNodes: [
|
||||
{
|
||||
nodeUuid,
|
||||
nodeEphemeralId,
|
||||
nodeName,
|
||||
},
|
||||
{
|
||||
nodeUuid: 'removedNodeId',
|
||||
nodeEphemeralId: 'removedNodeEmpheralId',
|
||||
nodeName: 'removedNodeName',
|
||||
},
|
||||
],
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: "Elasticsearch nodes 'newNodeName' added to this cluster. Elasticsearch nodes 'removedNodeName' removed from this cluster. Elasticsearch nodes 'test' restarted in this cluster.",
|
||||
},
|
||||
severity: 'warning',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
severity: 'warning',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
action: '[View nodes](elasticsearch/nodes)',
|
||||
actionPlain: 'Verify that you added, removed, or restarted nodes.',
|
||||
internalFullMessage:
|
||||
'Nodes changed alert is firing for testCluster. The following Elasticsearch nodes have been added:newNodeName removed:removedNodeName restarted:test. [View nodes](elasticsearch/nodes)',
|
||||
internalShortMessage:
|
||||
'Nodes changed alert is firing for testCluster. Verify that you added, removed, or restarted nodes.',
|
||||
added: 'newNodeName',
|
||||
removed: 'removedNodeName',
|
||||
restarted: 'test',
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'abc123',
|
||||
context: {
|
||||
action: '[View nodes](elasticsearch/nodes)',
|
||||
actionPlain: 'Verify that you added, removed, or restarted nodes.',
|
||||
internalFullMessage:
|
||||
'Nodes changed alert is firing for testCluster. The following Elasticsearch nodes have been added:newNodeName removed:removedNodeName restarted:test. [View nodes](elasticsearch/nodes)',
|
||||
internalShortMessage:
|
||||
'Nodes changed alert is firing for testCluster. Verify that you added, removed, or restarted nodes.',
|
||||
added: 'newNodeName',
|
||||
removed: 'removedNodeName',
|
||||
restarted: 'test',
|
||||
clusterName,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]:
|
||||
'Nodes changed alert is firing for testCluster. Verify that you added, removed, or restarted nodes.',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -323,8 +332,8 @@ describe('NodesChangedAlert', () => {
|
|||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).not.toHaveBeenCalledWith({});
|
||||
expect(scheduleActions).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.report).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.setAlertData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { Alert } from '@kbn/alerting-plugin/server';
|
||||
import { SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { AlertInstanceContext, SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { BaseRule } from './base_rule';
|
||||
import {
|
||||
AlertData,
|
||||
|
@ -174,7 +176,13 @@ export class NodesChangedRule extends BaseRule {
|
|||
}
|
||||
|
||||
protected async executeActions(
|
||||
instance: Alert,
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
alertId: string,
|
||||
{ alertStates }: AlertInstanceState,
|
||||
item: AlertData | null,
|
||||
cluster: AlertCluster
|
||||
|
@ -198,37 +206,44 @@ export class NodesChangedRule extends BaseRule {
|
|||
const added = states.added.map((node) => node.nodeName).join(',');
|
||||
const removed = states.removed.map((node) => node.nodeName).join(',');
|
||||
const restarted = states.restarted.map((node) => node.nodeName).join(',');
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage',
|
||||
{
|
||||
defaultMessage: `Nodes changed alert is firing for {clusterName}. {shortActionText}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
shortActionText,
|
||||
},
|
||||
}
|
||||
),
|
||||
internalFullMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.nodesChanged.firing.internalFullMessage',
|
||||
{
|
||||
defaultMessage: `Nodes changed alert is firing for {clusterName}. The following Elasticsearch nodes have been added:{added} removed:{removed} restarted:{restarted}. {action}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
added,
|
||||
removed,
|
||||
restarted,
|
||||
action,
|
||||
},
|
||||
}
|
||||
),
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
clusterName: cluster.clusterName,
|
||||
added,
|
||||
removed,
|
||||
restarted,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
const internalShortMessage = i18n.translate(
|
||||
'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage',
|
||||
{
|
||||
defaultMessage: `Nodes changed alert is firing for {clusterName}. {shortActionText}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
shortActionText,
|
||||
},
|
||||
}
|
||||
);
|
||||
services.alertsClient?.setAlertData({
|
||||
id: alertId,
|
||||
context: {
|
||||
internalShortMessage,
|
||||
internalFullMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.nodesChanged.firing.internalFullMessage',
|
||||
{
|
||||
defaultMessage: `Nodes changed alert is firing for {clusterName}. The following Elasticsearch nodes have been added:{added} removed:{removed} restarted:{restarted}. {action}`,
|
||||
values: {
|
||||
clusterName: cluster.clusterName,
|
||||
added,
|
||||
removed,
|
||||
restarted,
|
||||
action,
|
||||
},
|
||||
}
|
||||
),
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
clusterName: cluster.clusterName,
|
||||
added,
|
||||
removed,
|
||||
restarted,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: internalShortMessage,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AlertsFactory } from './alerts_factory';
|
||||
import { RulesFactory } from './rules_factory';
|
||||
import { RULE_CPU_USAGE } from '../../common/constants';
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
|
@ -16,7 +16,7 @@ jest.mock('../static_globals', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
describe('AlertsFactory', () => {
|
||||
describe('RulesFactory', () => {
|
||||
const rulesClient = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
@ -39,7 +39,7 @@ describe('AlertsFactory', () => {
|
|||
],
|
||||
};
|
||||
});
|
||||
const alerts = await AlertsFactory.getByType(RULE_CPU_USAGE, rulesClient as any);
|
||||
const alerts = await RulesFactory.getByType(RULE_CPU_USAGE, rulesClient as any);
|
||||
expect(alerts).not.toBeNull();
|
||||
expect(alerts.length).toBe(2);
|
||||
expect(alerts[0].getId()).toBe(1);
|
||||
|
@ -54,7 +54,7 @@ describe('AlertsFactory', () => {
|
|||
total: 0,
|
||||
};
|
||||
});
|
||||
await AlertsFactory.getByType(RULE_CPU_USAGE, rulesClient as any);
|
||||
await RulesFactory.getByType(RULE_CPU_USAGE, rulesClient as any);
|
||||
expect(filter).toBe(`alert.attributes.alertTypeId:${RULE_CPU_USAGE}`);
|
||||
});
|
||||
});
|
|
@ -40,7 +40,7 @@ import {
|
|||
RULE_CCR_READ_EXCEPTIONS,
|
||||
RULE_LARGE_SHARD_SIZE,
|
||||
} from '../../common/constants';
|
||||
import { CommonAlertParams } from '../../common/types/alerts';
|
||||
import { CommonAlertParams as CommonRuleParams } from '../../common/types/alerts';
|
||||
|
||||
const BY_TYPE = {
|
||||
[RULE_CLUSTER_HEALTH]: ClusterHealthRule,
|
||||
|
@ -59,28 +59,28 @@ const BY_TYPE = {
|
|||
[RULE_LARGE_SHARD_SIZE]: LargeShardSizeRule,
|
||||
};
|
||||
|
||||
export class AlertsFactory {
|
||||
export class RulesFactory {
|
||||
public static async getByType(
|
||||
type: string,
|
||||
alertsClient: RulesClient | undefined
|
||||
rulesClient: RulesClient | undefined
|
||||
): Promise<BaseRule[]> {
|
||||
const alertCls = BY_TYPE[type];
|
||||
if (!alertCls || !alertsClient) {
|
||||
const ruleCls = BY_TYPE[type];
|
||||
if (!ruleCls || !rulesClient) {
|
||||
return [];
|
||||
}
|
||||
const alertClientAlerts = await alertsClient.find<CommonAlertParams>({
|
||||
const rulesClientRules = await rulesClient.find<CommonRuleParams>({
|
||||
options: {
|
||||
filter: `alert.attributes.alertTypeId:${type}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!alertClientAlerts.total || !alertClientAlerts.data?.length) {
|
||||
if (!rulesClientRules.total || !rulesClientRules.data?.length) {
|
||||
return [];
|
||||
}
|
||||
return alertClientAlerts.data.map((alert) => new alertCls(alert as Rule) as BaseRule);
|
||||
return rulesClientRules.data.map((rule) => new ruleCls(rule as Rule) as BaseRule);
|
||||
}
|
||||
|
||||
public static getAll() {
|
||||
return Object.values(BY_TYPE).map((alert) => new alert());
|
||||
return Object.values(BY_TYPE).map((rule) => new rule());
|
||||
}
|
||||
}
|
|
@ -7,8 +7,15 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { Alert } from '@kbn/alerting-plugin/server';
|
||||
import { Rule, RawAlertInstance } from '@kbn/alerting-plugin/common';
|
||||
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
|
||||
import {
|
||||
Rule,
|
||||
RawAlertInstance,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { BaseRule } from './base_rule';
|
||||
import {
|
||||
AlertData,
|
||||
|
@ -176,7 +183,13 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule {
|
|||
};
|
||||
}
|
||||
protected executeActions(
|
||||
instance: Alert,
|
||||
services: RuleExecutorServices<
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default',
|
||||
DefaultAlert
|
||||
>,
|
||||
alertId: string,
|
||||
{ alertStates }: { alertStates: AlertState[] },
|
||||
item: AlertData | null,
|
||||
cluster: AlertCluster
|
||||
|
@ -243,19 +256,25 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule {
|
|||
}
|
||||
);
|
||||
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage,
|
||||
internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage,
|
||||
threadPoolType: type,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
/* continue to send "count" value for users before https://github.com/elastic/kibana/pull/102544
|
||||
services.alertsClient?.setAlertData({
|
||||
id: alertId,
|
||||
context: {
|
||||
internalShortMessage,
|
||||
internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage,
|
||||
threadPoolType: type,
|
||||
state: AlertingDefaults.ALERT_STATE.firing,
|
||||
/* continue to send "count" value for users before https://github.com/elastic/kibana/pull/102544
|
||||
see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431
|
||||
*/
|
||||
count: 1,
|
||||
node: nodeName,
|
||||
clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
count: 1,
|
||||
node: nodeName,
|
||||
clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: internalShortMessage,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,406 @@
|
|||
/*
|
||||
* 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 { ThreadPoolSearchRejectionsRule } from './thread_pool_search_rejections_rule';
|
||||
import { RULE_THREAD_POOL_SEARCH_REJECTIONS } from '../../common/constants';
|
||||
import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_thread_pool_rejections_stats', () => ({
|
||||
fetchThreadPoolRejectionStats: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
show_license_expiration: true,
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ThreadpoolSearchRejectionsRule', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new ThreadPoolSearchRejectionsRule();
|
||||
expect(rule.ruleOptions.id).toBe(RULE_THREAD_POOL_SEARCH_REJECTIONS);
|
||||
expect(rule.ruleOptions.name).toBe('Thread pool search rejections');
|
||||
expect(rule.ruleOptions.throttle).toBe('1d');
|
||||
expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' });
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'node', description: 'The node reporting high thread pool search rejections.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
describe('execute', () => {
|
||||
function FakeDate() {}
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const nodeId = 'esNode1';
|
||||
const nodeName = 'esName1';
|
||||
const threadPoolType = 'search';
|
||||
const rejectionCount = 400;
|
||||
const stat = [
|
||||
{
|
||||
rejectionCount,
|
||||
type: threadPoolType,
|
||||
clusterUuid,
|
||||
nodeId,
|
||||
nodeName,
|
||||
ccs: null,
|
||||
},
|
||||
];
|
||||
|
||||
const services = alertsMock.createRuleExecutorServices();
|
||||
const executorOptions = { services, state: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
Date = FakeDate;
|
||||
(fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => {
|
||||
return stat;
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fire action', async () => {
|
||||
const rule = new ThreadPoolSearchRejectionsRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'esNode1',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: null,
|
||||
cluster: { clusterUuid, clusterName },
|
||||
nodeId,
|
||||
nodeName,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
rejectionCount,
|
||||
clusterUuid,
|
||||
type: threadPoolType,
|
||||
nodeId,
|
||||
nodeName,
|
||||
ccs: null,
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: `Node #start_link${nodeName}#end_link is reporting ${rejectionCount} ${threadPoolType} rejections at #absolute`,
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkMonitor this node#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes/esNode1/advanced',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkOptimize complex queries#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}blog/advanced-tuning-finding-and-fixing-slow-elasticsearch-queries',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkAdd more nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkResize your deployment (ECE)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkThread pool settings#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: `elasticsearch/nodes/${nodeId}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'esNode1',
|
||||
context: {
|
||||
internalFullMessage: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
node: `${nodeName}`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: `Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
clusterName,
|
||||
count: 1,
|
||||
threadPoolType,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should not fire actions if under threshold', async () => {
|
||||
(fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat[0],
|
||||
rejectionCount: 1,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new ThreadPoolSearchRejectionsRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.setAlertData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat[0],
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new ThreadPoolSearchRejectionsRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'esNode1',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: 'testCluster',
|
||||
cluster: { clusterUuid, clusterName },
|
||||
nodeId,
|
||||
nodeName,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
rejectionCount,
|
||||
clusterUuid,
|
||||
type: threadPoolType,
|
||||
nodeId,
|
||||
nodeName,
|
||||
ccs: 'testCluster',
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: `Node #start_link${nodeName}#end_link is reporting ${rejectionCount} ${threadPoolType} rejections at #absolute`,
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkMonitor this node#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes/esNode1/advanced',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkOptimize complex queries#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}blog/advanced-tuning-finding-and-fixing-slow-elasticsearch-queries',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkAdd more nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkResize your deployment (ECE)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkThread pool settings#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: `elasticsearch/nodes/${nodeId}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'esNode1',
|
||||
context: {
|
||||
internalFullMessage: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`,
|
||||
internalShortMessage: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
node: `${nodeName}`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/esNode1?_g=(cluster_uuid:abc123,ccs:testCluster))`,
|
||||
actionPlain: `Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
clusterName,
|
||||
count,
|
||||
state: 'firing',
|
||||
threadPoolType,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,406 @@
|
|||
/*
|
||||
* 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 { ThreadPoolWriteRejectionsRule } from './thread_pool_write_rejections_rule';
|
||||
import { RULE_THREAD_POOL_WRITE_REJECTIONS } from '../../common/constants';
|
||||
import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_thread_pool_rejections_stats', () => ({
|
||||
fetchThreadPoolRejectionStats: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../static_globals', () => ({
|
||||
Globals: {
|
||||
app: {
|
||||
getLogger: () => ({ debug: jest.fn() }),
|
||||
url: 'http://localhost:5601',
|
||||
config: {
|
||||
ui: {
|
||||
show_license_expiration: true,
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ThreadpoolWriteRejectionsAlert', () => {
|
||||
it('should have defaults', () => {
|
||||
const rule = new ThreadPoolWriteRejectionsRule();
|
||||
expect(rule.ruleOptions.id).toBe(RULE_THREAD_POOL_WRITE_REJECTIONS);
|
||||
expect(rule.ruleOptions.name).toBe(`Thread pool write rejections`);
|
||||
expect(rule.ruleOptions.throttle).toBe('1d');
|
||||
expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' });
|
||||
expect(rule.ruleOptions.actionVariables).toStrictEqual([
|
||||
{ name: 'node', description: 'The node reporting high thread pool write rejections.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the node(s) belongs.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
describe('execute', () => {
|
||||
function FakeDate() {}
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const nodeId = 'esNode1';
|
||||
const nodeName = 'esName1';
|
||||
const threadPoolType = 'write';
|
||||
const rejectionCount = 400;
|
||||
const stat = [
|
||||
{
|
||||
rejectionCount,
|
||||
type: threadPoolType,
|
||||
clusterUuid,
|
||||
nodeId,
|
||||
nodeName,
|
||||
ccs: null,
|
||||
},
|
||||
];
|
||||
|
||||
const services = alertsMock.createRuleExecutorServices();
|
||||
const executorOptions = { services, state: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
Date = FakeDate;
|
||||
(fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => {
|
||||
return stat;
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fire action', async () => {
|
||||
const rule = new ThreadPoolWriteRejectionsRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'esNode1',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: null,
|
||||
cluster: { clusterUuid, clusterName },
|
||||
nodeId,
|
||||
nodeName,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
rejectionCount,
|
||||
clusterUuid,
|
||||
type: threadPoolType,
|
||||
nodeId,
|
||||
nodeName,
|
||||
ccs: null,
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: `Node #start_link${nodeName}#end_link is reporting ${rejectionCount} ${threadPoolType} rejections at #absolute`,
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkMonitor this node#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes/esNode1/advanced',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkOptimize complex queries#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}blog/advanced-tuning-finding-and-fixing-slow-elasticsearch-queries',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkAdd more nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkResize your deployment (ECE)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkThread pool settings#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: `elasticsearch/nodes/${nodeId}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'esNode1',
|
||||
context: {
|
||||
internalFullMessage: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
internalShortMessage: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
node: `${nodeName}`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`,
|
||||
actionPlain: `Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
clusterName,
|
||||
count: 1,
|
||||
threadPoolType,
|
||||
state: 'firing',
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should not fire actions if under threshold', async () => {
|
||||
(fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat[0],
|
||||
rejectionCount: 1,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new ThreadPoolWriteRejectionsRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
expect(services.alertsClient.report).not.toHaveBeenCalled();
|
||||
expect(services.alertsClient.setAlertData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...stat[0],
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const rule = new ThreadPoolWriteRejectionsRule();
|
||||
const type = rule.getRuleType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
params: rule.ruleOptions.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(services.alertsClient.report).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1);
|
||||
expect(services.alertsClient.report).toHaveBeenCalledWith({
|
||||
id: 'esNode1',
|
||||
actionGroup: 'default',
|
||||
state: {
|
||||
alertStates: [
|
||||
{
|
||||
ccs: 'testCluster',
|
||||
cluster: { clusterUuid, clusterName },
|
||||
nodeId,
|
||||
nodeName,
|
||||
itemLabel: undefined,
|
||||
meta: {
|
||||
rejectionCount,
|
||||
clusterUuid,
|
||||
type: threadPoolType,
|
||||
nodeId,
|
||||
nodeName,
|
||||
ccs: 'testCluster',
|
||||
},
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text: `Node #start_link${nodeName}#end_link is reporting ${rejectionCount} ${threadPoolType} rejections at #absolute`,
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkMonitor this node#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes/esNode1/advanced',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkOptimize complex queries#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}blog/advanced-tuning-finding-and-fixing-slow-elasticsearch-queries',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkAdd more nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkResize your deployment (ECE)#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '#start_linkThread pool settings#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'docLink',
|
||||
partialUrl:
|
||||
'{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: `elasticsearch/nodes/${nodeId}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({
|
||||
id: 'esNode1',
|
||||
context: {
|
||||
internalFullMessage: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`,
|
||||
internalShortMessage: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
node: `${nodeName}`,
|
||||
action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/esNode1?_g=(cluster_uuid:abc123,ccs:testCluster))`,
|
||||
actionPlain: `Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
clusterName,
|
||||
count,
|
||||
state: 'firing',
|
||||
threadPoolType,
|
||||
},
|
||||
payload: {
|
||||
[ALERT_REASON]: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -42,6 +42,8 @@
|
|||
"@kbn/observability-shared-plugin",
|
||||
"@kbn/shared-ux-link-redirect-app",
|
||||
"@kbn/logs-shared-plugin",
|
||||
"@kbn/alerts-as-data-utils",
|
||||
"@kbn/rule-data-utils",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue