From 4d58d1d9756e627506965f975d2a4844d420bd3d Mon Sep 17 00:00:00 2001 From: Peter Kosztolanyi Date: Tue, 14 Jul 2020 13:59:03 +0200 Subject: [PATCH] [AP-531] Add VictorOps alert handler (#469) --- dev-project/pipelinewise-config/config.yml | 4 ++ docs/user_guide/alerts.rst | 44 ++++++++++++- .../alert_handlers/victorops_alert_handler.py | 61 +++++++++++++++++++ pipelinewise/cli/alert_sender.py | 4 +- pipelinewise/cli/samples/config.yml | 5 ++ pipelinewise/cli/schemas/config.json | 19 +++++- tests/units/cli/test_alert_sender.py | 23 +++++++ 7 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 pipelinewise/cli/alert_handlers/victorops_alert_handler.py diff --git a/dev-project/pipelinewise-config/config.yml b/dev-project/pipelinewise-config/config.yml index 9b4e85d6d..6c29419b8 100644 --- a/dev-project/pipelinewise-config/config.yml +++ b/dev-project/pipelinewise-config/config.yml @@ -7,3 +7,7 @@ alert_handlers: # slack: # token: "slack-token" # channel: "#slack-channel" + +# victorops: +# base_url: "https://alert.victorops.com/integrations/generic/.../alert/.../..." +# routing_key: "victorops-routing-key" diff --git a/docs/user_guide/alerts.rst b/docs/user_guide/alerts.rst index 07615216d..a3ed91455 100644 --- a/docs/user_guide/alerts.rst +++ b/docs/user_guide/alerts.rst @@ -19,6 +19,7 @@ to the alert handler. Currently available alert handlers: * :ref:`slack_alert_handler` + * :ref:`victorops_alert_handler` .. _slack_alert_handler: @@ -34,7 +35,13 @@ To send alerts to a Slack channel on failed tap runs: 3. Invite the bot to the channel by the ``/invite `` slack command. -4. Configure main ``config.yml`` +4. Configure the main ``config.yml`` + + **Config parameters**: + + ``token``: Slack bot user token + + ``channel``: Slack channel where the alerts will be sent .. code-block:: bash @@ -44,3 +51,38 @@ To send alerts to a Slack channel on failed tap runs: slack: token: "slack-token" channel: "#slack-channel" + + + +.. _victorops_alert_handler: + +VictorOps Alert Handler +----------------------- + +To send alerts and open an incident on VictorOps: + +1. Follow the instructions at `Enable the VictorOps REST Endpoint `_ and get the long notify URL. + +2. Find your routing key in VictorOps settings page + +3. Configure the main ``config.yml``: + + **Config parameters**: + + ``base_url``: The VictorOps notify URL **without** the routing key + + ``routing_key``: VictorOps routing key + +.. code-block:: bash + + --- + + alert_handlers: + victorops: + base_url: "https://alert.victorops.com/integrations/generic/.../alert/.../..." + routing_key: "victorops-routing-key" + +.. warning:: + + Make sure the VictorOps ``base_url`` **does not include** the ``routing_key``. + diff --git a/pipelinewise/cli/alert_handlers/victorops_alert_handler.py b/pipelinewise/cli/alert_handlers/victorops_alert_handler.py new file mode 100644 index 000000000..0713bff60 --- /dev/null +++ b/pipelinewise/cli/alert_handlers/victorops_alert_handler.py @@ -0,0 +1,61 @@ +""" +PipelineWise CLI - VictorOps alert handler +""" +import json +import requests + +from .errors import InvalidAlertHandlerException +from .base_alert_handler import BaseAlertHandler + +# Map alert levels to victorops compatible message types +ALERT_LEVEL_MESSAGE_TYPES = { + BaseAlertHandler.LOG: 'INFO', + BaseAlertHandler.INFO: 'INFO', + BaseAlertHandler.WARNING: 'WARNING', + BaseAlertHandler.ERROR: 'CRITICAL' +} + + +# pylint: disable=too-few-public-methods +class VictoropsAlertHandler(BaseAlertHandler): + """ + VictorOps Alert Handler class + """ + def __init__(self, config: dict) -> None: + if config is not None: + if 'base_url' not in config: + raise InvalidAlertHandlerException('Missing REST Endpoint URL in VictorOps connection') + self.base_url = config['base_url'] + + if 'routing_key' not in config: + raise InvalidAlertHandlerException('Missing routing key in VictorOps connection') + self.routing_key = config['routing_key'] + + else: + raise InvalidAlertHandlerException('No valid VictorOps config supplied.') + + def send(self, message: str, level: str = BaseAlertHandler.ERROR, exc: Exception = None) -> None: + """ + Send alert + + Args: + message: the alert message + level: alert level + exc: optional exception that triggered the alert + + Returns: + Initialised alert handler object + """ + # Send alert to VictorOps REST Endpoint as a HTTP post request + response = requests.post( + f'{self.base_url}/{self.routing_key}', + data=json.dumps({ + 'message_type': ALERT_LEVEL_MESSAGE_TYPES.get(level, BaseAlertHandler.ERROR), + 'entity_display_name': message, + 'state_message': exc}), + headers={'Content-Type': 'application/json'}) + + # Success victorops message should return 200 + if response.status_code != 200: + raise ValueError('Request to victorops returned an error {}. {}'.format(response.status_code, + response.text)) diff --git a/pipelinewise/cli/alert_sender.py b/pipelinewise/cli/alert_sender.py index 8d0b798ef..6a428e093 100644 --- a/pipelinewise/cli/alert_sender.py +++ b/pipelinewise/cli/alert_sender.py @@ -7,6 +7,7 @@ from .alert_handlers.base_alert_handler import BaseAlertHandler from .alert_handlers.slack_alert_handler import SlackAlertHandler +from .alert_handlers.victorops_alert_handler import VictoropsAlertHandler from .alert_handlers.errors import InvalidAlertHandlerException from .alert_handlers.errors import NotImplementedAlertHandlerException @@ -21,7 +22,8 @@ # The key is the alert handler name from the PPW config.yml # Every alert handler class needs to implement the BaseAlertHandler base class ALERT_HANDLER_TYPES_TO_CLASS = { - 'slack': SlackAlertHandler + 'slack': SlackAlertHandler, + 'victorops': VictoropsAlertHandler } diff --git a/pipelinewise/cli/samples/config.yml b/pipelinewise/cli/samples/config.yml index 9b4e85d6d..4483c12e4 100644 --- a/pipelinewise/cli/samples/config.yml +++ b/pipelinewise/cli/samples/config.yml @@ -7,3 +7,8 @@ alert_handlers: # slack: # token: "slack-token" # channel: "#slack-channel" + +# victorops: +# base_url: "https://alert.victorops.com/integrations/generic/.../alert/.../..." +# routing_key: "victorops-routing-key" + diff --git a/pipelinewise/cli/schemas/config.json b/pipelinewise/cli/schemas/config.json index 09694db00..8f747ee88 100644 --- a/pipelinewise/cli/schemas/config.json +++ b/pipelinewise/cli/schemas/config.json @@ -15,6 +15,22 @@ "channel" ], "additionalProperties": false + }, + "alert_victorops": { + "type": "object", + "properties": { + "base_url": { + "type": "string" + }, + "routing_key": { + "type": "string" + } + }, + "required": [ + "base_url", + "routing_key" + ], + "additionalProperties": false } }, "type": ["object", "null"], @@ -22,7 +38,8 @@ "alert_handlers": { "type": ["object", "null"], "properties": { - "slack": { "$ref": "#/definitions/alert_slack" } + "slack": { "$ref": "#/definitions/alert_slack" }, + "victorops": { "$ref": "#/definitions/alert_victorops" } }, "additionalProperties": false } diff --git a/tests/units/cli/test_alert_sender.py b/tests/units/cli/test_alert_sender.py index adf24f3c3..9ca7ad852 100644 --- a/tests/units/cli/test_alert_sender.py +++ b/tests/units/cli/test_alert_sender.py @@ -1,4 +1,5 @@ import pytest +import collections from unittest.mock import patch from slack.errors import SlackApiError @@ -6,6 +7,7 @@ from pipelinewise.cli.alert_sender import AlertHandler, AlertSender from pipelinewise.cli.alert_handlers.slack_alert_handler import SlackAlertHandler +from pipelinewise.cli.alert_handlers.victorops_alert_handler import VictoropsAlertHandler # pylint: disable=no-self-use,too-few-public-methods @@ -102,3 +104,24 @@ def test_slack_handler(self): slack_post_message_mock.return_value = [] slack = SlackAlertHandler({'token': 'valid-token', 'channel': '#my-channel'}) slack.send('test message') + + def test_victorops_handler(self): + """Functions to test victorops alert handler""" + # Should raise an exception if no base url and routing_key provided + with pytest.raises(errors.InvalidAlertHandlerException): + VictoropsAlertHandler({'no-victorops-url': 'no-url'}) + + # Should raise an exception if no base_url provided + with pytest.raises(errors.InvalidAlertHandlerException): + VictoropsAlertHandler({'routing_key': 'some-routing-key'}) + + # Should raise an exception if no routing_key provided + with pytest.raises(errors.InvalidAlertHandlerException): + VictoropsAlertHandler({'base_url': 'some-url'}) + + # Should send alert if valid victorops REST endpoint URL provided + with patch('requests.post') as victorops_post_message_mock: + VictorOpsResponseMock = collections.namedtuple('VictorOpsResponseMock', 'status_code') + victorops_post_message_mock.return_value = VictorOpsResponseMock(status_code=200) + victorops = VictoropsAlertHandler({'base_url': 'some-url', 'routing_key': 'some-routing-key'}) + victorops.send('test message')