[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:
Ying Mao 2024-01-15 11:49:00 -05:00 committed by GitHub
parent 7a4f08a831
commit c8d0ae5b9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 4454 additions and 2899 deletions

View file

@ -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 {

View file

@ -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,
});
});
});
});

View file

@ -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',
});
});
});
});

View file

@ -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',
});
});
});
});

View file

@ -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,
});
});
});
});

View file

@ -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);
});
});
});

View file

@ -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',
});
});
});
});

View file

@ -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',
});
});
});
});

View file

@ -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,
});
});
});
});

View file

@ -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,
});
});
});
});

View file

@ -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 {};

View file

@ -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);
});
});

View file

@ -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>>());

View file

@ -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

View file

@ -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
) {

View file

@ -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.`,
},
});
});
});
});

View file

@ -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,
},
});
}
}

View file

@ -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();
});
});
});

View file

@ -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,
},
});
}
}

View 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.`,
},
});
});
});
});

View file

@ -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,
},
});
}
}

View 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.`,
},
});
});
});
});

View file

@ -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,
},
});
}
}

View file

@ -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();
});
});
});

View file

@ -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,
},
});
}
}

View file

@ -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';

View file

@ -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();
});
});
});

View file

@ -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,
},
});
}
}

View file

@ -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.`,
},
});
});
});
});

View file

@ -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,
},
});
}
}

View file

@ -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.',
},
});
});
});
});

View file

@ -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,
},
});
}
}

View file

@ -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();
});
});
});

View file

@ -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,
},
});
}
}

View 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.`,
},
});
});
});
});

View file

@ -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,
},
});
}
}

View file

@ -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.`,
},
});
});
});
});

View file

@ -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,
},
});
}
}

View file

@ -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();
});
});
});

View file

@ -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,
},
});
}
}

View file

@ -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}`);
});
});

View file

@ -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());
}
}

View file

@ -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,
},
});
}
}

View file

@ -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.`,
},
});
});
});
});

View file

@ -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.`,
},
});
});
});
});

View file

@ -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/**/*",