Skip to content

Commit

Permalink
[AP-531] Add VictorOps alert handler (#469)
Browse files Browse the repository at this point in the history
  • Loading branch information
koszti authored Jul 14, 2020
1 parent 80fdec3 commit 4d58d1d
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 3 deletions.
4 changes: 4 additions & 0 deletions dev-project/pipelinewise-config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
44 changes: 43 additions & 1 deletion docs/user_guide/alerts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ to the alert handler.

Currently available alert handlers:
* :ref:`slack_alert_handler`
* :ref:`victorops_alert_handler`


.. _slack_alert_handler:
Expand All @@ -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 <bot_name>`` 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
Expand All @@ -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 <https://help.victorops.com/knowledge-base/rest-endpoint-integration-guide/>`_ 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``.

61 changes: 61 additions & 0 deletions pipelinewise/cli/alert_handlers/victorops_alert_handler.py
Original file line number Diff line number Diff line change
@@ -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))
4 changes: 3 additions & 1 deletion pipelinewise/cli/alert_sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}


Expand Down
5 changes: 5 additions & 0 deletions pipelinewise/cli/samples/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

19 changes: 18 additions & 1 deletion pipelinewise/cli/schemas/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,31 @@
"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"],
"properties": {
"alert_handlers": {
"type": ["object", "null"],
"properties": {
"slack": { "$ref": "#/definitions/alert_slack" }
"slack": { "$ref": "#/definitions/alert_slack" },
"victorops": { "$ref": "#/definitions/alert_victorops" }
},
"additionalProperties": false
}
Expand Down
23 changes: 23 additions & 0 deletions tests/units/cli/test_alert_sender.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import pytest
import collections
from unittest.mock import patch
from slack.errors import SlackApiError

import pipelinewise.cli.alert_handlers.errors as errors

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
Expand Down Expand Up @@ -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')

0 comments on commit 4d58d1d

Please sign in to comment.