Skip to content

Commit

Permalink
Merge pull request #1282 from seanyinx/master
Browse files Browse the repository at this point in the history
Lark alerter support
  • Loading branch information
jertel authored Oct 8, 2023
2 parents 2008db1 + 04298fb commit 19880de
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/source/elastalert.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions docs/source/ruletypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~

Expand Down
53 changes: 53 additions & 0 deletions elastalert/alerters/lark.py
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions elastalert/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions elastalert/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down
141 changes: 141 additions & 0 deletions tests/alerters/lark_test.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 19880de

Please sign in to comment.