Skip to content

Commit 6ceeb2c

Browse files
authored
Merge pull request #1649 from pijiang3/master
Add Flashduty Alerter
2 parents 0646160 + fb26cdd commit 6ceeb2c

File tree

7 files changed

+289
-0
lines changed

7 files changed

+289
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [MicrosoftPowerAutomate] Add support for 'ms_power_automate_webhook_url_from_field' option to dynamically select the webhook URL from the match. - [#1623](https://github.com/jertel/elastalert2/pull/1623) - @aizerin
88
- Add Webex Incoming Webhook alerter - [#1635](https://github.com/jertel/elastalert2/pull/1635) - @dennis-trapp
99
- Support jinja2 templates in `alertmanager_labels` and `alertmanager_annotations` - [#1642](https://github.com/jertel/elastalert2/pull/1642) - @tgxworld
10+
- Add Flashduty alerter - [#1649](https://github.com/jertel/elastalert2/pull/1649) - @pijiang3
1011

1112
## Other changes
1213
- Fix `schema.yaml` to support Kibana 8.17 - [#1631](https://github.com/jertel/elastalert2/pull/1631) - @vpiserchia

docs/source/alerts.rst

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ or
2626
- discord
2727
- email
2828
- exotel
29+
- flashduty
2930
- gitter
3031
- googlechat
3132
- gelf
@@ -2618,3 +2619,39 @@ Example usage::
26182619
- "yzj"
26192620
yzj_token: "token"
26202621

2622+
2623+
Flashduty
2624+
~~~~~~~~~~~~~
2625+
2626+
Flashduty alerter will send notification to a Flashduty application. The body of the notification formatted the same as with other alerters.
2627+
2628+
Required:
2629+
2630+
``flashduty_integration_key``: Flashduty integration key.
2631+
``flashduty_title``: Alert title , no more than 512 characters, will be truncated if exceeded. Default to ``ElastAlert Alert``.
2632+
``flashduty_event_status``: Alert status. Can be ``Info``, ``Warning``, ``Critical``, ``Ok``. Defaults to ``Info``.
2633+
2634+
2635+
Example usage::
2636+
2637+
alert_text: "**{0}** - ALERT on host {1}"
2638+
alert_text_args:
2639+
- name
2640+
- hostname
2641+
alert:
2642+
- flashduty
2643+
flashduty_integration_key: "xxx"
2644+
flashduty_title: "elastalert"
2645+
flashduty_event_status: "Warning"
2646+
flashduty_alert_key: "abc"
2647+
flashduty_description: "log error"
2648+
flashduty_check: "Too many occurrences of error logs"
2649+
flashduty_resource: "index_name"
2650+
flashduty_service: "service_name"
2651+
flashduty_metric: "The number of error logs is greater than 5"
2652+
flashduty_group: "sre"
2653+
flashduty_cluster: "k8s"
2654+
flashduty_app: "app"
2655+
flashduty_env: "dev"
2656+
2657+
Please refer to the parameter definition: https://docs.flashcat.cloud/en/flashduty/elastalert2-integration-guide

docs/source/elastalert.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Currently, we have support built in for these alert types:
3939
- Discord
4040
- Email
4141
- Exotel
42+
- Flashduty
4243
- Gitter
4344
- GoogleChat
4445
- Graylog GELF

elastalert/alerters/flashduty.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import json
2+
import warnings
3+
4+
import requests
5+
from elastalert.alerts import Alerter, DateTimeEncoder
6+
from elastalert.util import EAException, elastalert_logger
7+
from requests import RequestException
8+
9+
10+
class FlashdutyAlerter(Alerter):
11+
"""Creates a Flashduty message for each alert"""
12+
13+
required_options = frozenset(["flashduty_integration_key"])
14+
15+
def __init__(self, rule):
16+
super(FlashdutyAlerter, self).__init__(rule)
17+
self.flashduty_integration_key = self.rule.get("flashduty_integration_key", None)
18+
self.flashduty_title = self.rule.get("flashduty_title", "ElastAlert Alert")
19+
self.flashduty_alert_key = self.rule.get("flashduty_alert_key", None)
20+
self.flashduty_description = self.rule.get("flashduty_description", None)
21+
self.flashduty_event_status = self.rule.get("flashduty_event_status", "Info")
22+
self.flashduty_check = self.rule.get("flashduty_check", None)
23+
self.flashduty_service = self.rule.get("flashduty_service", None)
24+
self.flashduty_cluster = self.rule.get("flashduty_cluster", None)
25+
self.flashduty_resource = self.rule.get("flashduty_resource", None)
26+
self.flashduty_metric = self.rule.get("flashduty_metric", None)
27+
self.flashduty_group = self.rule.get("flashduty_group", None)
28+
self.flashduty_env = self.rule.get("flashduty_env", None)
29+
self.flashduty_app = self.rule.get("flashduty_app", None)
30+
31+
32+
def alert(self, matches):
33+
body = self.create_alert_body(matches)
34+
35+
headers = {
36+
"Content-Type": "application/json",
37+
}
38+
39+
payload = {
40+
"title": self.flashduty_title,
41+
"description": self.flashduty_description,
42+
"event_status": self.flashduty_event_status,
43+
"alert_key": self.flashduty_alert_key,
44+
"labels": {
45+
"check": self.flashduty_check,
46+
"service": self.flashduty_service,
47+
"cluster": self.flashduty_cluster,
48+
"resource": self.flashduty_resource,
49+
"metric": self.flashduty_metric,
50+
"group": self.flashduty_group,
51+
"env": self.flashduty_env,
52+
"app": self.flashduty_app,
53+
"information": body,
54+
}
55+
}
56+
57+
try:
58+
response = requests.post(
59+
"https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + self.flashduty_integration_key,
60+
data=json.dumps(payload, cls=DateTimeEncoder),
61+
headers=headers,
62+
)
63+
warnings.resetwarnings()
64+
response.raise_for_status()
65+
except RequestException as e:
66+
raise EAException("Error posting to flashduty: %s" % e)
67+
68+
elastalert_logger.info("Trigger sent to flashduty")
69+
70+
def get_info(self):
71+
return {
72+
"type": "flashduty",
73+
"flashduty_integration_key": self.flashduty_integration_key,
74+
"flashduty_title": self.flashduty_title,
75+
"flashduty_description": self.flashduty_description,
76+
"flashduty_event_status": self.flashduty_event_status,
77+
"flashduty_check": self.flashduty_check,
78+
"flashduty_service": self.flashduty_service,
79+
"flashduty_cluster": self.flashduty_cluster,
80+
"flashduty_resource": self.flashduty_resource,
81+
"flashduty_metric": self.flashduty_metric,
82+
"flashduty_group": self.flashduty_group,
83+
"flashduty_env": self.flashduty_env,
84+
"flashduty_app": self.flashduty_app,
85+
"flashduty_alert_key": self.flashduty_alert_key,
86+
}

elastalert/loaders.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import jsonschema
99
import yaml
1010
import yaml.scanner
11+
from elastalert.alerters.flashduty import FlashdutyAlerter
1112
from jinja2 import Environment
1213
from jinja2 import FileSystemLoader
1314
from jinja2 import Template
@@ -145,6 +146,7 @@ class RulesLoader(object):
145146
'indexer': IndexerAlerter,
146147
'matrixhookshot': MatrixHookshotAlerter,
147148
'yzj': YzjAlerter,
149+
'flashduty': FlashdutyAlerter,
148150
}
149151

150152
# A partial ordering of alert types. Relative order will be preserved in the resulting alerts list

elastalert/schema.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,3 +917,17 @@ properties:
917917
yzj_custom_loc: {type: string}
918918
yzj_proxy: {type: string}
919919

920+
### Flashduty
921+
flashduty_integration_key: {type: string}
922+
flashduty_title: {type: string}
923+
flashduty_description: {type: string}
924+
flashduty_event_status: {type: string, enum: [Info, Warning, Critical, Ok]}
925+
flashduty_alert_key: {type: string}
926+
flashduty_check: {type: string}
927+
flashduty_service: {type: string}
928+
flashduty_cluster: {type: string}
929+
flashduty_resource: {type: string}
930+
flashduty_metric: {type: string}
931+
flashduty_group: {type: string}
932+
flashduty_env: {type: string}
933+
flashduty_app: {type: string}

tests/alerters/flashduty_test.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import json
2+
import logging
3+
from unittest import mock
4+
5+
import pytest
6+
from requests import RequestException
7+
8+
from elastalert.alerters.flashduty import FlashdutyAlerter
9+
from elastalert.loaders import FileRulesLoader
10+
from elastalert.util import EAException
11+
12+
13+
def test_flashduty_alert(caplog):
14+
caplog.set_level(logging.INFO)
15+
rule = {
16+
'name': 'Test Flashduty Rule',
17+
'type': 'any',
18+
'flashduty_integration_key': 'xxx',
19+
'flashduty_title': 'Test Alert',
20+
'flashduty_description': 'Test Description',
21+
'flashduty_event_status': 'Info',
22+
'flashduty_alert_key': 'test-alert',
23+
'flashduty_check': 'test-check',
24+
'flashduty_service': 'test-service',
25+
'flashduty_cluster': 'test-cluster',
26+
'flashduty_resource': 'test-resource',
27+
'flashduty_metric': 'test-metric',
28+
'flashduty_group': 'test-group',
29+
'flashduty_env': 'test-env',
30+
'flashduty_app': 'test-app',
31+
'alert': [],
32+
}
33+
rules_loader = FileRulesLoader({})
34+
rules_loader.load_modules(rule)
35+
alert = FlashdutyAlerter(rule)
36+
match = {
37+
'@timestamp': '2025-03-20T00:00:00',
38+
'somefield': 'foobar'
39+
}
40+
41+
with mock.patch('requests.post') as mock_post_request:
42+
alert.alert([match])
43+
44+
expected_data = {
45+
'title': 'Test Alert',
46+
'description': 'Test Description',
47+
'event_status': 'Info',
48+
'alert_key': 'test-alert',
49+
'labels': {
50+
'check': 'test-check',
51+
'service': 'test-service',
52+
'cluster': 'test-cluster',
53+
'resource': 'test-resource',
54+
'metric': 'test-metric',
55+
'group': 'test-group',
56+
'env': 'test-env',
57+
'app': 'test-app',
58+
'information': 'Test Flashduty Rule\n\n@timestamp: 2025-03-20T00:00:00\nsomefield: foobar\n'
59+
}
60+
}
61+
62+
mock_post_request.assert_called_once_with(
63+
'https://api.flashcat.cloud/event/push/alert/standard?integration_key=' + rule['flashduty_integration_key'],
64+
data=mock.ANY,
65+
headers={'Content-Type': 'application/json'}
66+
)
67+
68+
actual_data = json.loads(mock_post_request.call_args_list[0][1]['data'])
69+
assert expected_data == actual_data
70+
assert ('elastalert', logging.INFO, 'Trigger sent to flashduty') == caplog.record_tuples[0]
71+
72+
73+
def test_flashduty_ea_exception():
74+
rule = {
75+
'name': 'Test Flashduty Rule',
76+
'type': 'any',
77+
'flashduty_integration_key': 'xxx',
78+
'alert': [],
79+
}
80+
rules_loader = FileRulesLoader({})
81+
rules_loader.load_modules(rule)
82+
alert = FlashdutyAlerter(rule)
83+
match = {
84+
'@timestamp': '2025-03-20T00:00:00',
85+
'somefield': 'foobar'
86+
}
87+
mock_run = mock.MagicMock(side_effect=RequestException)
88+
with mock.patch('requests.post', mock_run):
89+
with pytest.raises(EAException) as ea:
90+
alert.alert([match])
91+
assert 'Error posting to flashduty: ' in str(ea)
92+
93+
94+
def test_flashduty_getinfo():
95+
rule = {
96+
'name': 'Test Flashduty Rule',
97+
'type': 'any',
98+
'flashduty_integration_key': 'xxx',
99+
'flashduty_title': 'Test Alert',
100+
'flashduty_description': 'Test Description',
101+
'flashduty_event_status': 'Info',
102+
'flashduty_alert_key': 'test-alert',
103+
'flashduty_check': 'test-check',
104+
'flashduty_service': 'test-service',
105+
'flashduty_cluster': 'test-cluster',
106+
'flashduty_resource': 'test-resource',
107+
'flashduty_metric': 'test-metric',
108+
'flashduty_group': 'test-group',
109+
'flashduty_env': 'test-env',
110+
'flashduty_app': 'test-app',
111+
'alert': [],
112+
}
113+
rules_loader = FileRulesLoader({})
114+
rules_loader.load_modules(rule)
115+
alert = FlashdutyAlerter(rule)
116+
117+
expected_data = {
118+
'type': 'flashduty',
119+
'flashduty_integration_key': 'xxx',
120+
'flashduty_title': 'Test Alert',
121+
'flashduty_description': 'Test Description',
122+
'flashduty_event_status': 'Info',
123+
'flashduty_alert_key': 'test-alert',
124+
'flashduty_check': 'test-check',
125+
'flashduty_service': 'test-service',
126+
'flashduty_cluster': 'test-cluster',
127+
'flashduty_resource': 'test-resource',
128+
'flashduty_metric': 'test-metric',
129+
'flashduty_group': 'test-group',
130+
'flashduty_env': 'test-env',
131+
'flashduty_app': 'test-app'
132+
}
133+
actual_data = alert.get_info()
134+
assert expected_data == actual_data
135+
136+
137+
def test_flashduty_required_error():
138+
try:
139+
rule = {
140+
'name': 'Test Flashduty Rule',
141+
'type': 'any',
142+
'alert': [],
143+
}
144+
rules_loader = FileRulesLoader({})
145+
rules_loader.load_modules(rule)
146+
FlashdutyAlerter(rule)
147+
except Exception as ea:
148+
assert 'Missing required option(s): flashduty_integration_key' in str(ea)

0 commit comments

Comments
 (0)