Skip to content

Commit 19880de

Browse files
authored
Merge pull request #1282 from seanyinx/master
Lark alerter support
2 parents 2008db1 + 04298fb commit 19880de

File tree

7 files changed

+222
-0
lines changed

7 files changed

+222
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- Add support for Kibana 8.10 for Kibana Discover - [#1277](https://github.com/jertel/elastalert2/pull/1277) - @nsano-rururu
1414
- 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
1515
- Fix issue with aggregated alerts not being sent - [#1285](https://github.com/jertel/elastalert2/pull/1285) - @jertel
16+
- Add support for [Lark](https://www.larksuite.com/en_us/) alerter - [#1282](https://github.com/jertel/elastalert2/pull/1282) - @seanyinx
1617

1718
# 2.13.2
1819

docs/source/elastalert.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Currently, we have support built in for these alert types:
4545
- HTTP POST
4646
- HTTP POST 2
4747
- Jira
48+
- Lark
4849
- Line Notify
4950
- Mattermost
5051
- Microsoft Teams

docs/source/ruletypes.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2697,6 +2697,26 @@ Example usage::
26972697
- My Custom Value 1
26982698
- My Custom Value 2
26992699

2700+
Lark
2701+
~~~~~~~~
2702+
2703+
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.
2704+
2705+
Required:
2706+
2707+
``lark_bot_id``: Lark bot id.
2708+
2709+
Optional:
2710+
2711+
``lark_msgtype``: Lark msgtype, currently only ``text`` supported.
2712+
2713+
Example usage::
2714+
2715+
alert:
2716+
- "lark"
2717+
lark_bot_id: "your lark bot id"
2718+
lark_msgtype: "text"
2719+
27002720
Line Notify
27012721
~~~~~~~~~~~
27022722

elastalert/alerters/lark.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 LarkAlerter(Alerter):
11+
""" Creates a Lark message for each alert """
12+
required_options = frozenset(['lark_bot_id'])
13+
14+
def __init__(self, rule):
15+
super(LarkAlerter, self).__init__(rule)
16+
self.lark_bot_id = self.rule.get('lark_bot_id', None)
17+
self.lark_webhook_url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{self.lark_bot_id}'
18+
self.lark_msg_type = self.rule.get('lark_msgtype', 'text')
19+
20+
def alert(self, matches):
21+
title = self.create_title(matches)
22+
body = self.create_alert_body(matches)
23+
24+
headers = {
25+
'Content-Type': 'application/json',
26+
'Accept': 'application/json;charset=utf-8'
27+
}
28+
29+
payload = {
30+
'msg_type': self.lark_msg_type,
31+
"content": {
32+
"title": title,
33+
"text": body
34+
},
35+
}
36+
37+
try:
38+
response = requests.post(
39+
self.lark_webhook_url,
40+
data=json.dumps(payload, cls=DateTimeEncoder),
41+
headers=headers)
42+
warnings.resetwarnings()
43+
response.raise_for_status()
44+
except RequestException as e:
45+
raise EAException("Error posting to lark: %s" % e)
46+
47+
elastalert_logger.info("Trigger sent to lark")
48+
49+
def get_info(self):
50+
return {
51+
"type": "lark",
52+
"lark_webhook_url": self.lark_webhook_url
53+
}

elastalert/loaders.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import elastalert.alerters.googlechat
2626
import elastalert.alerters.httppost
2727
import elastalert.alerters.httppost2
28+
import elastalert.alerters.lark
2829
import elastalert.alerters.line
2930
import elastalert.alerters.pagertree
3031
import elastalert.alerters.rocketchat
@@ -126,6 +127,7 @@ class RulesLoader(object):
126127
'zabbix': ZabbixAlerter,
127128
'discord': elastalert.alerters.discord.DiscordAlerter,
128129
'dingtalk': elastalert.alerters.dingtalk.DingTalkAlerter,
130+
'lark': elastalert.alerters.lark.LarkAlerter,
129131
'chatwork': elastalert.alerters.chatwork.ChatworkAlerter,
130132
'datadog': elastalert.alerters.datadog.DatadogAlerter,
131133
'ses': elastalert.alerters.ses.SesAlerter,

elastalert/schema.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,10 @@ properties:
536536
jira_transition_to: {type: string}
537537
jira_bump_after_inactivity: {type: number}
538538

539+
### Lark
540+
lark_bot_id: { type: string }
541+
lark_msgtype: { type: string, enum: [ 'text' ] }
542+
539543
### Line Notify
540544
linenotify_access_token: {type: string}
541545

tests/alerters/lark_test.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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.lark import LarkAlerter
9+
from elastalert.loaders import FileRulesLoader
10+
from elastalert.util import EAException
11+
12+
13+
def test_lark_text(caplog):
14+
caplog.set_level(logging.INFO)
15+
rule = {
16+
'name': 'Test Lark Rule',
17+
'type': 'any',
18+
'lark_bot_id': 'xxxxxxx',
19+
'lark_msgtype': 'text',
20+
'alert': [],
21+
'alert_subject': 'Test Lark'
22+
}
23+
rules_loader = FileRulesLoader({})
24+
rules_loader.load_modules(rule)
25+
alert = LarkAlerter(rule)
26+
match = {
27+
'@timestamp': '2021-01-01T00:00:00',
28+
'somefield': 'foobarbaz'
29+
}
30+
with mock.patch('requests.post') as mock_post_request:
31+
alert.alert([match])
32+
33+
expected_data = {
34+
'msg_type': 'text',
35+
'content': {
36+
'title': 'Test Lark',
37+
'text': 'Test Lark Rule\n\n@timestamp: 2021-01-01T00:00:00\nsomefield: foobarbaz\n'
38+
}
39+
}
40+
41+
mock_post_request.assert_called_once_with(
42+
'https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxx',
43+
data=mock.ANY,
44+
headers={
45+
'Content-Type': 'application/json',
46+
'Accept': 'application/json;charset=utf-8'
47+
}
48+
)
49+
50+
actual_data = json.loads(mock_post_request.call_args_list[0][1]['data'])
51+
assert expected_data == actual_data
52+
assert ('elastalert', logging.INFO, 'Trigger sent to lark') == caplog.record_tuples[0]
53+
54+
55+
def test_lark_ea_exception():
56+
with pytest.raises(EAException) as ea:
57+
rule = {
58+
'name': 'Test Lark Rule',
59+
'type': 'any',
60+
'lark_bot_id': 'xxxxxxx',
61+
'lark_msgtype': 'action_card',
62+
'lark_single_title': 'elastalert',
63+
'lark_single_url': 'http://xxxxx2',
64+
'lark_btn_orientation': '1',
65+
'lark_btns': [
66+
{
67+
'title': 'test1',
68+
'actionURL': 'https://xxxxx0/'
69+
},
70+
{
71+
'title': 'test2',
72+
'actionURL': 'https://xxxxx1/'
73+
}
74+
],
75+
'lark_proxy': 'http://proxy.url',
76+
'lark_proxy_login': 'admin',
77+
'lark_proxy_pass': 'password',
78+
'alert': [],
79+
'alert_subject': 'Test Lark'
80+
}
81+
rules_loader = FileRulesLoader({})
82+
rules_loader.load_modules(rule)
83+
alert = LarkAlerter(rule)
84+
match = {
85+
'@timestamp': '2021-01-01T00:00:00',
86+
'somefield': 'foobarbaz'
87+
}
88+
mock_run = mock.MagicMock(side_effect=RequestException)
89+
with mock.patch('requests.post', mock_run), pytest.raises(RequestException):
90+
alert.alert([match])
91+
assert 'Error posting to lark: ' in str(ea)
92+
93+
94+
def test_lark_getinfo():
95+
rule = {
96+
'name': 'Test Lark Rule',
97+
'type': 'any',
98+
'lark_bot_id': 'xxxxxxx',
99+
'alert': [],
100+
'alert_subject': 'Test Lark'
101+
}
102+
rules_loader = FileRulesLoader({})
103+
rules_loader.load_modules(rule)
104+
alert = LarkAlerter(rule)
105+
106+
expected_data = {
107+
'type': 'lark',
108+
"lark_webhook_url": 'https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxx'
109+
}
110+
actual_data = alert.get_info()
111+
assert expected_data == actual_data
112+
113+
114+
@pytest.mark.parametrize('lark_bot_id, expected_data', [
115+
('', 'Missing required option(s): lark_bot_id'),
116+
('xxxxxxx',
117+
{
118+
'type': 'lark',
119+
"lark_webhook_url": 'https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxx'
120+
}),
121+
])
122+
def test_lark_required_error(lark_bot_id, expected_data):
123+
try:
124+
rule = {
125+
'name': 'Test Lark Rule',
126+
'type': 'any',
127+
'alert': [],
128+
'alert_subject': 'Test Lark'
129+
}
130+
131+
if lark_bot_id:
132+
rule['lark_bot_id'] = lark_bot_id
133+
134+
rules_loader = FileRulesLoader({})
135+
rules_loader.load_modules(rule)
136+
alert = LarkAlerter(rule)
137+
138+
actual_data = alert.get_info()
139+
assert expected_data == actual_data
140+
except Exception as ea:
141+
assert expected_data in str(ea)

0 commit comments

Comments
 (0)