diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e46383b..22e32c1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Add support for Kibana 8.10 for Kibana Discover - [#1277](https://github.com/jertel/elastalert2/pull/1277) - @nsano-rururu - Upgrade pylint 2.17.4 to 2.17.5, pytest 7.3.1 to 7.4.2, sphinx 6.2.1 to 7.2.6, sphinx_rtd_theme 1.2.2 to 1.3.0 - [#1278](https://github.com/jertel/elastalert2/pull/1278) - @nsano-rururu - Fix issue with aggregated alerts not being sent - [#1285](https://github.com/jertel/elastalert2/pull/1285) - @jertel +- Add support for [Lark](https://www.larksuite.com/en_us/) alerter - [#1282](https://github.com/jertel/elastalert2/pull/1282) - @seanyinx # 2.13.2 diff --git a/docs/source/elastalert.rst b/docs/source/elastalert.rst index ecfdcffb..e989092f 100755 --- a/docs/source/elastalert.rst +++ b/docs/source/elastalert.rst @@ -45,6 +45,7 @@ Currently, we have support built in for these alert types: - HTTP POST - HTTP POST 2 - Jira +- Lark - Line Notify - Mattermost - Microsoft Teams diff --git a/docs/source/ruletypes.rst b/docs/source/ruletypes.rst index a289fe63..21ee81ca 100644 --- a/docs/source/ruletypes.rst +++ b/docs/source/ruletypes.rst @@ -2697,6 +2697,26 @@ Example usage:: - My Custom Value 1 - My Custom Value 2 +Lark +~~~~~~~~ + +Lark alerter will send notification to a predefined bot in Lark application. The body of the notification is formatted the same as with other alerters. + +Required: + +``lark_bot_id``: Lark bot id. + +Optional: + +``lark_msgtype``: Lark msgtype, currently only ``text`` supported. + +Example usage:: + + alert: + - "lark" + lark_bot_id: "your lark bot id" + lark_msgtype: "text" + Line Notify ~~~~~~~~~~~ diff --git a/elastalert/alerters/lark.py b/elastalert/alerters/lark.py new file mode 100644 index 00000000..e74db445 --- /dev/null +++ b/elastalert/alerters/lark.py @@ -0,0 +1,53 @@ +import json +import warnings + +import requests +from elastalert.alerts import Alerter, DateTimeEncoder +from elastalert.util import EAException, elastalert_logger +from requests import RequestException + + +class LarkAlerter(Alerter): + """ Creates a Lark message for each alert """ + required_options = frozenset(['lark_bot_id']) + + def __init__(self, rule): + super(LarkAlerter, self).__init__(rule) + self.lark_bot_id = self.rule.get('lark_bot_id', None) + self.lark_webhook_url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{self.lark_bot_id}' + self.lark_msg_type = self.rule.get('lark_msgtype', 'text') + + def alert(self, matches): + title = self.create_title(matches) + body = self.create_alert_body(matches) + + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json;charset=utf-8' + } + + payload = { + 'msg_type': self.lark_msg_type, + "content": { + "title": title, + "text": body + }, + } + + try: + response = requests.post( + self.lark_webhook_url, + data=json.dumps(payload, cls=DateTimeEncoder), + headers=headers) + warnings.resetwarnings() + response.raise_for_status() + except RequestException as e: + raise EAException("Error posting to lark: %s" % e) + + elastalert_logger.info("Trigger sent to lark") + + def get_info(self): + return { + "type": "lark", + "lark_webhook_url": self.lark_webhook_url + } diff --git a/elastalert/loaders.py b/elastalert/loaders.py index 994beddf..4e0524a5 100644 --- a/elastalert/loaders.py +++ b/elastalert/loaders.py @@ -25,6 +25,7 @@ import elastalert.alerters.googlechat import elastalert.alerters.httppost import elastalert.alerters.httppost2 +import elastalert.alerters.lark import elastalert.alerters.line import elastalert.alerters.pagertree import elastalert.alerters.rocketchat @@ -126,6 +127,7 @@ class RulesLoader(object): 'zabbix': ZabbixAlerter, 'discord': elastalert.alerters.discord.DiscordAlerter, 'dingtalk': elastalert.alerters.dingtalk.DingTalkAlerter, + 'lark': elastalert.alerters.lark.LarkAlerter, 'chatwork': elastalert.alerters.chatwork.ChatworkAlerter, 'datadog': elastalert.alerters.datadog.DatadogAlerter, 'ses': elastalert.alerters.ses.SesAlerter, diff --git a/elastalert/schema.yaml b/elastalert/schema.yaml index b24bfb83..5a479dc9 100644 --- a/elastalert/schema.yaml +++ b/elastalert/schema.yaml @@ -536,6 +536,10 @@ properties: jira_transition_to: {type: string} jira_bump_after_inactivity: {type: number} + ### Lark + lark_bot_id: { type: string } + lark_msgtype: { type: string, enum: [ 'text' ] } + ### Line Notify linenotify_access_token: {type: string} diff --git a/tests/alerters/lark_test.py b/tests/alerters/lark_test.py new file mode 100644 index 00000000..c8ad1aec --- /dev/null +++ b/tests/alerters/lark_test.py @@ -0,0 +1,141 @@ +import json +import logging +from unittest import mock + +import pytest +from requests import RequestException + +from elastalert.alerters.lark import LarkAlerter +from elastalert.loaders import FileRulesLoader +from elastalert.util import EAException + + +def test_lark_text(caplog): + caplog.set_level(logging.INFO) + rule = { + 'name': 'Test Lark Rule', + 'type': 'any', + 'lark_bot_id': 'xxxxxxx', + 'lark_msgtype': 'text', + 'alert': [], + 'alert_subject': 'Test Lark' + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = LarkAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + with mock.patch('requests.post') as mock_post_request: + alert.alert([match]) + + expected_data = { + 'msg_type': 'text', + 'content': { + 'title': 'Test Lark', + 'text': 'Test Lark Rule\n\n@timestamp: 2021-01-01T00:00:00\nsomefield: foobarbaz\n' + } + } + + mock_post_request.assert_called_once_with( + 'https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxx', + data=mock.ANY, + headers={ + 'Content-Type': 'application/json', + 'Accept': 'application/json;charset=utf-8' + } + ) + + actual_data = json.loads(mock_post_request.call_args_list[0][1]['data']) + assert expected_data == actual_data + assert ('elastalert', logging.INFO, 'Trigger sent to lark') == caplog.record_tuples[0] + + +def test_lark_ea_exception(): + with pytest.raises(EAException) as ea: + rule = { + 'name': 'Test Lark Rule', + 'type': 'any', + 'lark_bot_id': 'xxxxxxx', + 'lark_msgtype': 'action_card', + 'lark_single_title': 'elastalert', + 'lark_single_url': 'http://xxxxx2', + 'lark_btn_orientation': '1', + 'lark_btns': [ + { + 'title': 'test1', + 'actionURL': 'https://xxxxx0/' + }, + { + 'title': 'test2', + 'actionURL': 'https://xxxxx1/' + } + ], + 'lark_proxy': 'http://proxy.url', + 'lark_proxy_login': 'admin', + 'lark_proxy_pass': 'password', + 'alert': [], + 'alert_subject': 'Test Lark' + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = LarkAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + assert 'Error posting to lark: ' in str(ea) + + +def test_lark_getinfo(): + rule = { + 'name': 'Test Lark Rule', + 'type': 'any', + 'lark_bot_id': 'xxxxxxx', + 'alert': [], + 'alert_subject': 'Test Lark' + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = LarkAlerter(rule) + + expected_data = { + 'type': 'lark', + "lark_webhook_url": 'https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxx' + } + actual_data = alert.get_info() + assert expected_data == actual_data + + +@pytest.mark.parametrize('lark_bot_id, expected_data', [ + ('', 'Missing required option(s): lark_bot_id'), + ('xxxxxxx', + { + 'type': 'lark', + "lark_webhook_url": 'https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxx' + }), +]) +def test_lark_required_error(lark_bot_id, expected_data): + try: + rule = { + 'name': 'Test Lark Rule', + 'type': 'any', + 'alert': [], + 'alert_subject': 'Test Lark' + } + + if lark_bot_id: + rule['lark_bot_id'] = lark_bot_id + + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = LarkAlerter(rule) + + actual_data = alert.get_info() + assert expected_data == actual_data + except Exception as ea: + assert expected_data in str(ea)