diff --git a/docs/source/ruletypes.rst b/docs/source/ruletypes.rst index 0f6a4af3..e8217fb8 100644 --- a/docs/source/ruletypes.rst +++ b/docs/source/ruletypes.rst @@ -3324,6 +3324,8 @@ Required: ``zbx_sender_port``: The port where zabbix server is listenning, defaults to ``10051``. +``zbx_host_from_field``: This field allows to specify ``zbx_host`` value from the available terms. Defaults to ``False``. + ``zbx_host``: This field setup the host in zabbix that receives the value sent by ElastAlert 2. ``zbx_key``: This field setup the key in the host that receives the value sent by ElastAlert 2. @@ -3336,3 +3338,17 @@ Example usage:: zbx_sender_port: 10051 zbx_host: "test001" zbx_key: "sender_load1" + +To specify ``zbx_host`` depending on the available elasticsearch field, zabbix alerter has ``zbx_host_from_field`` option. + +Example usage:: + + alert: + - "zabbix" + zbx_sender_host: "zabbix-server" + zbx_sender_port: 10051 + zbx_host_from_field: True + zbx_host: "hostname" + zbx_key: "sender_load1" + +where ``hostname`` is the available elasticsearch field. diff --git a/elastalert/alerters/zabbix.py b/elastalert/alerters/zabbix.py index 214bdb2c..b914fd44 100644 --- a/elastalert/alerters/zabbix.py +++ b/elastalert/alerters/zabbix.py @@ -3,13 +3,19 @@ from pyzabbix import ZabbixSender, ZabbixMetric, ZabbixAPI from elastalert.alerts import Alerter -from elastalert.util import elastalert_logger, EAException +from elastalert.util import elastalert_logger, lookup_es_key, EAException class ZabbixClient(ZabbixAPI): - - def __init__(self, url='http://localhost', use_authenticate=False, user='Admin', password='zabbix', - sender_host='localhost', sender_port=10051): + def __init__( + self, + url="http://localhost", + use_authenticate=False, + user="Admin", + password="zabbix", + sender_host="localhost", + sender_port=10051, + ): self.url = url self.use_authenticate = use_authenticate self.sender_host = sender_host @@ -17,26 +23,33 @@ def __init__(self, url='http://localhost', use_authenticate=False, user='Admin', self.metrics_chunk_size = 200 self.aggregated_metrics = [] - super(ZabbixClient, self).__init__(url=self.url, - use_authenticate=self.use_authenticate, - user=user, - password=password) + super(ZabbixClient, self).__init__( + url=self.url, + use_authenticate=self.use_authenticate, + user=user, + password=password, + ) def send_metric(self, hostname, key, data): zm = ZabbixMetric(hostname, key, data) if self.send_aggregated_metrics: self.aggregated_metrics.append(zm) if len(self.aggregated_metrics) > self.metrics_chunk_size: - elastalert_logger.info("Sending: %s metrics" % (len(self.aggregated_metrics))) + elastalert_logger.info( + "Sending: %s metrics" % (len(self.aggregated_metrics)) + ) try: - ZabbixSender(zabbix_server=self.sender_host, zabbix_port=self.sender_port) \ - .send(self.aggregated_metrics) + ZabbixSender( + zabbix_server=self.sender_host, zabbix_port=self.sender_port + ).send(self.aggregated_metrics) self.aggregated_metrics = [] except Exception as e: elastalert_logger.exception(e) else: try: - ZabbixSender(zabbix_server=self.sender_host, zabbix_port=self.sender_port).send([zm]) + ZabbixSender( + zabbix_server=self.sender_host, zabbix_port=self.sender_port + ).send([zm]) except Exception as e: elastalert_logger.exception(e) @@ -46,18 +59,21 @@ class ZabbixAlerter(Alerter): # You can ensure that the rule config file specifies all # of the options. Otherwise, ElastAlert will throw an exception # when trying to load the rule. - required_options = frozenset(['zbx_host', 'zbx_key']) + required_options = frozenset(["zbx_host", "zbx_key"]) def __init__(self, *args): super(ZabbixAlerter, self).__init__(*args) - self.zbx_sender_host = self.rule.get('zbx_sender_host', 'localhost') - self.zbx_sender_port = self.rule.get('zbx_sender_port', 10051) - self.zbx_host = self.rule.get('zbx_host', None) - self.zbx_key = self.rule.get('zbx_key', None) - self.timestamp_field = self.rule.get('timestamp_field', '@timestamp') - self.timestamp_type = self.rule.get('timestamp_type', 'iso') - self.timestamp_strptime = self.rule.get('timestamp_strptime', '%Y-%m-%dT%H:%M:%S.%f%z') + self.zbx_sender_host = self.rule.get("zbx_sender_host", "localhost") + self.zbx_sender_port = self.rule.get("zbx_sender_port", 10051) + self.zbx_host_from_field = self.rule.get("zbx_host_from_field", False) + self.zbx_host = self.rule.get("zbx_host", None) + self.zbx_key = self.rule.get("zbx_key", None) + self.timestamp_field = self.rule.get("timestamp_field", "@timestamp") + self.timestamp_type = self.rule.get("timestamp_type", "iso") + self.timestamp_strptime = self.rule.get( + "timestamp_strptime", "%Y-%m-%dT%H:%M:%S.%f%z" + ) # Alert is called def alert(self, matches): @@ -67,24 +83,57 @@ def alert(self, matches): # the aggregation option set zm = [] for match in matches: - if ':' not in match[self.timestamp_field] or '-' not in match[self.timestamp_field]: + if ( + ":" not in match[self.timestamp_field] + or "-" not in match[self.timestamp_field] + ): ts_epoch = int(match[self.timestamp_field]) else: try: - ts_epoch = int(datetime.strptime(match[self.timestamp_field], self.timestamp_strptime) - .timestamp()) + ts_epoch = int( + datetime.strptime( + match[self.timestamp_field], self.timestamp_strptime + ).timestamp() + ) except ValueError: - ts_epoch = int(datetime.strptime(match[self.timestamp_field], '%Y-%m-%dT%H:%M:%S%z') - .timestamp()) - zm.append(ZabbixMetric(host=self.zbx_host, key=self.zbx_key, value='1', clock=ts_epoch)) + ts_epoch = int( + datetime.strptime( + match[self.timestamp_field], "%Y-%m-%dT%H:%M:%S%z" + ).timestamp() + ) + if self.zbx_host_from_field: + zbx_host = lookup_es_key(match, self.rule["zbx_host"]) + else: + zbx_host = self.zbx_host + zm.append( + ZabbixMetric(host=zbx_host, key=self.zbx_key, value="1", clock=ts_epoch) + ) try: - response = ZabbixSender(zabbix_server=self.zbx_sender_host, zabbix_port=self.zbx_sender_port).send(zm) + response = ZabbixSender( + zabbix_server=self.zbx_sender_host, zabbix_port=self.zbx_sender_port + ).send(zm) if response.failed: - elastalert_logger.warning("Missing zabbix host '%s' or host's item '%s', alert will be discarded" - % (self.zbx_host, self.zbx_key)) + if self.zbx_host_from_field and not zbx_host: + elastalert_logger.warning( + "Missing term '%s' or host's item '%s', alert will be discarded" + % (self.zbx_host, self.zbx_key) + ) + else: + elastalert_logger.warning( + "Missing zabbix host '%s' or host's item '%s', alert will be discarded" + % (zbx_host, self.zbx_key) + ) else: - elastalert_logger.info("Alert sent to Zabbix") + elastalert_logger.info( + "Alert sent to '%s:%s' zabbix server, '%s' zabbix host, '%s' zabbix host key" + % ( + self.zbx_sender_host, + self.zbx_sender_port, + zbx_host, + self.zbx_key, + ) + ) except Exception as e: raise EAException("Error sending alert to Zabbix: %s" % e) @@ -92,4 +141,4 @@ def alert(self, matches): # to Elasticsearch in the field "alert_info" # It should return a dict of information relevant to what the alert does def get_info(self): - return {'type': 'zabbix Alerter'} + return {"type": "zabbix Alerter"} diff --git a/tests/alerters/zabbix_test.py b/tests/alerters/zabbix_test.py index ca0fc09b..c982bc2e 100644 --- a/tests/alerters/zabbix_test.py +++ b/tests/alerters/zabbix_test.py @@ -11,81 +11,146 @@ def test_zabbix_basic(caplog): caplog.set_level(logging.WARNING) rule = { - 'name': 'Basic Zabbix test', - 'type': 'any', - 'alert_text_type': 'alert_text_only', - 'alert': [], - 'alert_subject': 'Test Zabbix', - 'zbx_host': 'example.com', - 'zbx_key': 'example-key' + "name": "Basic Zabbix test", + "type": "any", + "alert_text_type": "alert_text_only", + "alert": [], + "alert_subject": "Test Zabbix", + "zbx_host": "example.com", + "zbx_key": "example-key", } rules_loader = FileRulesLoader({}) rules_loader.load_modules(rule) alert = ZabbixAlerter(rule) - match = { - '@timestamp': '2021-01-01T00:00:00Z', - 'somefield': 'foobarbaz' - } - with mock.patch('pyzabbix.ZabbixSender.send') as mock_zbx_send: + match = {"@timestamp": "2021-01-01T00:00:00Z", "somefield": "foobarbaz"} + with mock.patch("pyzabbix.ZabbixSender.send") as mock_zbx_send: alert.alert([match]) zabbix_metrics = { "host": "example.com", "key": "example-key", "value": "1", - "clock": 1609459200 + "clock": 1609459200, } alerter_args = mock_zbx_send.call_args.args assert vars(alerter_args[0][0]) == zabbix_metrics log_messeage = "Missing zabbix host 'example.com' or host's item 'example-key', alert will be discarded" - assert ('elastalert', logging.WARNING, log_messeage) == caplog.record_tuples[0] + assert ("elastalert", logging.WARNING, log_messeage) == caplog.record_tuples[0] + + +@pytest.mark.parametrize( + "zbx_host_from_field, zbx_host, zbx_key, log_messeage", + [ + ( + True, + "hostname", + "example-key", + "Missing zabbix host 'example.com' or host's item 'example-key', alert will be discarded", + ), + ( + True, + "unavailable_field", + "example-key", + "Missing term 'unavailable_field' or host's item 'example-key', alert will be discarded", + ), + ( + False, + "hostname", + "example-key", + "Missing zabbix host 'hostname' or host's item 'example-key', alert will be discarded", + ), + ( + False, + "unavailable_field", + "example-key", + "Missing zabbix host 'unavailable_field' or host's item 'example-key', alert will be discarded", + ), + ], +) +def test_zabbix_enhanced(caplog, zbx_host_from_field, zbx_host, zbx_key, log_messeage): + caplog.set_level(logging.WARNING) + rule = { + "name": "Enhanced Zabbix test", + "type": "any", + "alert_text_type": "alert_text_only", + "alert": [], + "alert_subject": "Test Zabbix", + "zbx_host_from_field": zbx_host_from_field, + "zbx_host": zbx_host, + "zbx_key": zbx_key, + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = ZabbixAlerter(rule) + match = { + "@timestamp": "2021-01-01T00:00:00Z", + "somefield": "foobarbaz", + "hostname": "example.com", + } + with mock.patch("pyzabbix.ZabbixSender.send") as mock_zbx_send: + alert.alert([match]) + + hosts = { + (True, "hostname"): "example.com", + (True, "unavailable_field"): "None", + (False, "hostname"): "hostname", + (False, "unavailable_field"): "unavailable_field", + } + + zabbix_metrics = { + "host": hosts[(zbx_host_from_field, zbx_host)], + "key": "example-key", + "value": "1", + "clock": 1609459200, + } + alerter_args = mock_zbx_send.call_args.args + assert vars(alerter_args[0][0]) == zabbix_metrics + assert ("elastalert", logging.WARNING, log_messeage) == caplog.record_tuples[0] def test_zabbix_getinfo(): rule = { - 'name': 'Basic Zabbix test', - 'type': 'any', - 'alert_text_type': 'alert_text_only', - 'alert': [], - 'alert_subject': 'Test Zabbix', - 'zbx_host': 'example.com', - 'zbx_key': 'example-key' + "name": "Basic Zabbix test", + "type": "any", + "alert_text_type": "alert_text_only", + "alert": [], + "alert_subject": "Test Zabbix", + "zbx_host": "example.com", + "zbx_key": "example-key", } rules_loader = FileRulesLoader({}) rules_loader.load_modules(rule) alert = ZabbixAlerter(rule) - expected_data = { - 'type': 'zabbix Alerter' - } + expected_data = {"type": "zabbix Alerter"} actual_data = alert.get_info() assert expected_data == actual_data -@pytest.mark.parametrize('zbx_host, zbx_key, expected_data', [ - ('', '', 'Missing required option(s): zbx_host, zbx_key'), - ('example.com', '', 'Missing required option(s): zbx_host, zbx_key'), - ('', 'example-key', 'Missing required option(s): zbx_host, zbx_key'), - ('example.com', 'example-key', - { - 'type': 'zabbix Alerter' - }) -]) +@pytest.mark.parametrize( + "zbx_host, zbx_key, expected_data", + [ + ("", "", "Missing required option(s): zbx_host, zbx_key"), + ("example.com", "", "Missing required option(s): zbx_host, zbx_key"), + ("", "example-key", "Missing required option(s): zbx_host, zbx_key"), + ("example.com", "example-key", {"type": "zabbix Alerter"}), + ], +) def test_zabbix_required_error(zbx_host, zbx_key, expected_data): try: rule = { - 'name': 'Basic Zabbix test', - 'type': 'any', - 'alert_text_type': 'alert_text_only', - 'alert': [], - 'alert_subject': 'Test Zabbix' + "name": "Basic Zabbix test", + "type": "any", + "alert_text_type": "alert_text_only", + "alert": [], + "alert_subject": "Test Zabbix", } if zbx_host: - rule['zbx_host'] = zbx_host + rule["zbx_host"] = zbx_host if zbx_key: - rule['zbx_key'] = zbx_key + rule["zbx_key"] = zbx_key rules_loader = FileRulesLoader({}) rules_loader.load_modules(rule) @@ -100,21 +165,21 @@ def test_zabbix_required_error(zbx_host, zbx_key, expected_data): def test_zabbix_ea_exception(): with pytest.raises(EAException) as ea: rule = { - 'name': 'Basic Zabbix test', - 'type': 'any', - 'alert_text_type': 'alert_text_only', - 'alert': [], - 'alert_subject': 'Test Zabbix', - 'zbx_host': 'example.com', - 'zbx_key': 'example-key' + "name": "Basic Zabbix test", + "type": "any", + "alert_text_type": "alert_text_only", + "alert": [], + "alert_subject": "Test Zabbix", + "zbx_host": "example.com", + "zbx_key": "example-key", } match = { - '@timestamp': '2021-01-01T00:00:00Z', - 'somefield': 'foobarbaz' + "@timestamp": "2021-01-01T00:00:00Z", + "somefield": "foobarbaz", } rules_loader = FileRulesLoader({}) rules_loader.load_modules(rule) alert = ZabbixAlerter(rule) alert.alert([match]) - assert 'Error sending alert to Zabbix: ' in str(ea) + assert "Error sending alert to Zabbix: " in str(ea)