Skip to content

Commit

Permalink
Merge pull request #174 from ferozsalam/migrate-alerters-with-tests
Browse files Browse the repository at this point in the history
Migrate remaining alerters out of alerts.py
  • Loading branch information
jertel authored May 21, 2021
2 parents edbc9bb + af2b228 commit 751f874
Show file tree
Hide file tree
Showing 25 changed files with 1,494 additions and 1,346 deletions.
118 changes: 118 additions & 0 deletions elastalert/alerters/alerta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import datetime
import json

import requests
from requests import RequestException

from elastalert.alerts import Alerter, DateTimeEncoder
from elastalert.util import lookup_es_key, EAException, elastalert_logger, resolve_string, ts_to_dt


class AlertaAlerter(Alerter):
""" Creates an Alerta event for each alert """
required_options = frozenset(['alerta_api_url'])

def __init__(self, rule):
super(AlertaAlerter, self).__init__(rule)

# Setup defaul parameters
self.url = self.rule.get('alerta_api_url', None)
self.api_key = self.rule.get('alerta_api_key', None)
self.timeout = self.rule.get('alerta_timeout', 86400)
self.use_match_timestamp = self.rule.get('alerta_use_match_timestamp', False)
self.use_qk_as_resource = self.rule.get('alerta_use_qk_as_resource', False)
self.verify_ssl = not self.rule.get('alerta_api_skip_ssl', False)
self.missing_text = self.rule.get('alert_missing_value', '<MISSING VALUE>')

# Fill up default values of the API JSON payload
self.severity = self.rule.get('alerta_severity', 'warning')
self.resource = self.rule.get('alerta_resource', 'elastalert')
self.environment = self.rule.get('alerta_environment', 'Production')
self.origin = self.rule.get('alerta_origin', 'elastalert')
self.service = self.rule.get('alerta_service', ['elastalert'])
self.text = self.rule.get('alerta_text', 'elastalert')
self.type = self.rule.get('alerta_type', 'elastalert')
self.event = self.rule.get('alerta_event', 'elastalert')
self.correlate = self.rule.get('alerta_correlate', [])
self.tags = self.rule.get('alerta_tags', [])
self.group = self.rule.get('alerta_group', '')
self.attributes_keys = self.rule.get('alerta_attributes_keys', [])
self.attributes_values = self.rule.get('alerta_attributes_values', [])
self.value = self.rule.get('alerta_value', '')

def alert(self, matches):
# Override the resource if requested
if self.use_qk_as_resource and 'query_key' in self.rule and lookup_es_key(matches[0], self.rule['query_key']):
self.resource = lookup_es_key(matches[0], self.rule['query_key'])

headers = {'content-type': 'application/json'}
if self.api_key is not None:
headers['Authorization'] = 'Key %s' % (self.rule['alerta_api_key'])
alerta_payload = self.get_json_payload(matches[0])

try:
response = requests.post(self.url, data=alerta_payload, headers=headers, verify=self.verify_ssl)
response.raise_for_status()
except RequestException as e:
raise EAException("Error posting to Alerta: %s" % e)
elastalert_logger.info("Alert sent to Alerta")

def create_default_title(self, matches):
title = '%s' % (self.rule['name'])
# If the rule has a query_key, add that value
if 'query_key' in self.rule:
qk = matches[0].get(self.rule['query_key'])
if qk:
title += '.%s' % (qk)
return title

def get_info(self):
return {'type': 'alerta',
'alerta_url': self.url}

def get_json_payload(self, match):
"""
Builds the API Create Alert body, as in
http://alerta.readthedocs.io/en/latest/api/reference.html#create-an-alert
For the values that could have references to fields on the match, resolve those references.
"""

# Using default text and event title if not defined in rule
alerta_text = self.rule['type'].get_match_str([match]) if self.text == '' else resolve_string(self.text, match, self.missing_text)
alerta_event = self.create_default_title([match]) if self.event == '' else resolve_string(self.event, match, self.missing_text)

match_timestamp = lookup_es_key(match, self.rule.get('timestamp_field', '@timestamp'))
if match_timestamp is None:
match_timestamp = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")
if self.use_match_timestamp:
createTime = ts_to_dt(match_timestamp).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
else:
createTime = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")

alerta_payload_dict = {
'resource': resolve_string(self.resource, match, self.missing_text),
'severity': resolve_string(self.severity, match),
'timeout': self.timeout,
'createTime': createTime,
'type': self.type,
'environment': resolve_string(self.environment, match, self.missing_text),
'origin': resolve_string(self.origin, match, self.missing_text),
'group': resolve_string(self.group, match, self.missing_text),
'event': alerta_event,
'text': alerta_text,
'value': resolve_string(self.value, match, self.missing_text),
'service': [resolve_string(a_service, match, self.missing_text) for a_service in self.service],
'tags': [resolve_string(a_tag, match, self.missing_text) for a_tag in self.tags],
'correlate': [resolve_string(an_event, match, self.missing_text) for an_event in self.correlate],
'attributes': dict(list(zip(self.attributes_keys,
[resolve_string(a_value, match, self.missing_text) for a_value in self.attributes_values]))),
'rawData': self.create_alert_body([match]),
}

try:
payload = json.dumps(alerta_payload_dict, cls=DateTimeEncoder)
except Exception as e:
raise Exception("Error building Alerta request: %s" % e)
return payload
47 changes: 47 additions & 0 deletions elastalert/alerters/chatwork.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import warnings

import requests
from requests import RequestException
from requests.auth import HTTPProxyAuth

from elastalert.alerts import Alerter
from elastalert.util import EAException, elastalert_logger


class ChatworkAlerter(Alerter):
""" Creates a Chatwork room message for each alert """
required_options = frozenset(['chatwork_apikey', 'chatwork_room_id'])

def __init__(self, rule):
super(ChatworkAlerter, self).__init__(rule)
self.chatwork_apikey = self.rule.get('chatwork_apikey')
self.chatwork_room_id = self.rule.get('chatwork_room_id')
self.url = 'https://api.chatwork.com/v2/rooms/%s/messages' % (self.chatwork_room_id)
self.chatwork_proxy = self.rule.get('chatwork_proxy', None)
self.chatwork_proxy_login = self.rule.get('chatwork_proxy_login', None)
self.chatwork_proxy_pass = self.rule.get('chatwork_proxy_pass', None)

def alert(self, matches):
body = self.create_alert_body(matches)

headers = {'X-ChatWorkToken': self.chatwork_apikey}
# set https proxy, if it was provided
proxies = {'https': self.chatwork_proxy} if self.chatwork_proxy else None
auth = HTTPProxyAuth(self.chatwork_proxy_login, self.chatwork_proxy_pass) if self.chatwork_proxy_login else None
params = {'body': body}

try:
response = requests.post(self.url, params=params, headers=headers, proxies=proxies, auth=auth)
warnings.resetwarnings()
response.raise_for_status()
except RequestException as e:
raise EAException("Error posting to Chattwork: %s. Details: %s" % (e, "" if e.response is None else e.response.text))

elastalert_logger.info(
"Alert sent to Chatwork room %s" % self.chatwork_room_id)

def get_info(self):
return {
"type": "chatwork",
"chatwork_room_id": self.chatwork_room_id
}
48 changes: 48 additions & 0 deletions elastalert/alerters/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import json
import subprocess

from elastalert.alerts import Alerter, DateTimeEncoder
from elastalert.util import elastalert_logger, resolve_string, EAException


class CommandAlerter(Alerter):
required_options = set(['command'])

def __init__(self, *args):
super(CommandAlerter, self).__init__(*args)

self.last_command = []

self.shell = False
if isinstance(self.rule['command'], str):
self.shell = True
if '%' in self.rule['command']:
elastalert_logger.warning('Warning! You could be vulnerable to shell injection!')
self.rule['command'] = [self.rule['command']]

def alert(self, matches):
# Format the command and arguments
try:
command = [resolve_string(command_arg, matches[0]) for command_arg in self.rule['command']]
self.last_command = command
except KeyError as e:
raise EAException("Error formatting command: %s" % (e))

# Run command and pipe data
try:
subp = subprocess.Popen(command, stdin=subprocess.PIPE, shell=self.shell)

if self.rule.get('pipe_match_json'):
match_json = json.dumps(matches, cls=DateTimeEncoder) + '\n'
stdout, stderr = subp.communicate(input=match_json.encode())
elif self.rule.get('pipe_alert_text'):
alert_text = self.create_alert_body(matches)
stdout, stderr = subp.communicate(input=alert_text.encode())
if self.rule.get("fail_on_non_zero_exit", False) and subp.wait():
raise EAException("Non-zero exit code while running command %s" % (' '.join(command)))
except OSError as e:
raise EAException("Error while running command %s: %s" % (' '.join(command), e))

def get_info(self):
return {'type': 'command',
'command': ' '.join(self.last_command)}
38 changes: 38 additions & 0 deletions elastalert/alerters/datadog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import json

import requests
from requests import RequestException

from elastalert.alerts import Alerter, DateTimeEncoder
from elastalert.util import EAException, elastalert_logger


class DatadogAlerter(Alerter):
''' Creates a Datadog Event for each alert '''
required_options = frozenset(['datadog_api_key', 'datadog_app_key'])

def __init__(self, rule):
super(DatadogAlerter, self).__init__(rule)
self.dd_api_key = self.rule.get('datadog_api_key', None)
self.dd_app_key = self.rule.get('datadog_app_key', None)

def alert(self, matches):
url = 'https://api.datadoghq.com/api/v1/events'
headers = {
'Content-Type': 'application/json',
'DD-API-KEY': self.dd_api_key,
'DD-APPLICATION-KEY': self.dd_app_key
}
payload = {
'title': self.create_title(matches),
'text': self.create_alert_body(matches)
}
try:
response = requests.post(url, data=json.dumps(payload, cls=DateTimeEncoder), headers=headers)
response.raise_for_status()
except RequestException as e:
raise EAException('Error posting event to Datadog: %s' % e)
elastalert_logger.info('Alert sent to Datadog')

def get_info(self):
return {'type': 'datadog'}
19 changes: 19 additions & 0 deletions elastalert/alerters/debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from elastalert.alerts import Alerter, BasicMatchString
from elastalert.util import elastalert_logger, lookup_es_key


class DebugAlerter(Alerter):
""" The debug alerter uses a Python logger (by default, alerting to terminal). """

def alert(self, matches):
qk = self.rule.get('query_key', None)
for match in matches:
if qk in match:
elastalert_logger.info(
'Alert for %s, %s at %s:' % (self.rule['name'], match[qk], lookup_es_key(match, self.rule['timestamp_field'])))
else:
elastalert_logger.info('Alert for %s at %s:' % (self.rule['name'], lookup_es_key(match, self.rule['timestamp_field'])))
elastalert_logger.info(str(BasicMatchString(self.rule, match)))

def get_info(self):
return {'type': 'debug'}
71 changes: 71 additions & 0 deletions elastalert/alerters/discord.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import json
import warnings

import requests
from requests import RequestException
from requests.auth import HTTPProxyAuth

from elastalert.alerts import Alerter, BasicMatchString
from elastalert.util import EAException, elastalert_logger


class DiscordAlerter(Alerter):
""" Created a Discord for each alert """
required_options = frozenset(['discord_webhook_url'])

def __init__(self, rule):
super(DiscordAlerter, self).__init__(rule)
self.discord_webhook_url = self.rule['discord_webhook_url']
self.discord_emoji_title = self.rule.get('discord_emoji_title', ':warning:')
self.discord_proxy = self.rule.get('discord_proxy', None)
self.discord_proxy_login = self.rule.get('discord_proxy_login', None)
self.discord_proxy_password = self.rule.get('discord_proxy_password', None)
self.discord_embed_color = self.rule.get('discord_embed_color', 0xffffff)
self.discord_embed_footer = self.rule.get('discord_embed_footer', None)
self.discord_embed_icon_url = self.rule.get('discord_embed_icon_url', None)

def alert(self, matches):
body = ''
title = u'%s' % (self.create_title(matches))
for match in matches:
body += str(BasicMatchString(self.rule, match))
if len(matches) > 1:
body += '\n----------------------------------------\n'
if len(body) > 2047:
body = body[0:1950] + '\n *message was cropped according to discord embed description limits!* '

body += '```'

proxies = {'https': self.discord_proxy} if self.discord_proxy else None
auth = HTTPProxyAuth(self.discord_proxy_login, self.discord_proxy_password) if self.discord_proxy_login else None
headers = {"Content-Type": "application/json"}

data = {}
data["content"] = "%s %s %s" % (self.discord_emoji_title, title, self.discord_emoji_title)
data["embeds"] = []
embed = {}
embed["description"] = "%s" % (body)
embed["color"] = (self.discord_embed_color)

if self.discord_embed_footer:
embed["footer"] = {}
embed["footer"]["text"] = (self.discord_embed_footer) if self.discord_embed_footer else None
embed["footer"]["icon_url"] = (self.discord_embed_icon_url) if self.discord_embed_icon_url else None
else:
None

data["embeds"].append(embed)

try:
response = requests.post(self.discord_webhook_url, data=json.dumps(data), headers=headers, proxies=proxies, auth=auth)
warnings.resetwarnings()
response.raise_for_status()
except RequestException as e:
raise EAException("Error posting to Discord: %s. Details: %s" % (e, "" if e.response is None else e.response.text))

elastalert_logger.info(
"Alert sent to the webhook %s" % self.discord_webhook_url)

def get_info(self):
return {'type': 'discord',
'discord_webhook_url': self.discord_webhook_url}
34 changes: 34 additions & 0 deletions elastalert/alerters/exotel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import sys

from exotel import Exotel
from requests import RequestException

from elastalert.alerts import Alerter
from elastalert.util import EAException, elastalert_logger


class ExotelAlerter(Alerter):
required_options = frozenset(['exotel_account_sid', 'exotel_auth_token', 'exotel_to_number', 'exotel_from_number'])

def __init__(self, rule):
super(ExotelAlerter, self).__init__(rule)
self.exotel_account_sid = self.rule['exotel_account_sid']
self.exotel_auth_token = self.rule['exotel_auth_token']
self.exotel_to_number = self.rule['exotel_to_number']
self.exotel_from_number = self.rule['exotel_from_number']
self.sms_body = self.rule.get('exotel_message_body', '')

def alert(self, matches):
client = Exotel(self.exotel_account_sid, self.exotel_auth_token)

try:
message_body = self.rule['name'] + self.sms_body
response = client.sms(self.rule['exotel_from_number'], self.rule['exotel_to_number'], message_body)
if response != 200:
raise EAException("Error posting to Exotel, response code is %s" % response)
except RequestException:
raise EAException("Error posting to Exotel").with_traceback(sys.exc_info()[2])
elastalert_logger.info("Trigger sent to Exotel")

def get_info(self):
return {'type': 'exotel', 'exotel_account': self.exotel_account_sid}
Loading

0 comments on commit 751f874

Please sign in to comment.