Python

sentry-mattermost and notification plugin for sentry

At Biekos we are using Sentry to monitor exceptions in our codebase, both for frontend/mobile and backend. It is a critical tool that allows us to act quickly upon any incident. We are also using an on-premise installation of Mattermost to communicate.

Therefore we wanted to have Sentry report issues into it. Unfortunately, the plugin was not functioning correctly with the latest version of Sentry (>9.11). This is how we fixed it and what we learned about from it.

Mattermost has a marketplace where it is possible to obtain many plugins. By searching "sentry", you will end up on github.

Regrettably, nobody updated the repository since 2018. Installing the plugin will lead to an error during configuration:

    TypeError: notify() got an unexpected keyword argument 'raise_exception'
File "sentry/plugins/bases/notify.py", line 147, in test_configuration_and_get_test_results
    test_results = self.test_configuration(project)
File "sentry/plugins/bases/notify.py", line 143, in test_configuration
    return self.notify(notification, raise_exception=True)

We have then forked the repository and fixed the issue, you can find a working version of the plugin here.

 pip2.7 install -e git+https://github.com/Biekos/sentry-mattermost.git@master#egg=sentry-mattermost

At Biekos, we believe in open source. Most of the tools we are using daily, Phabricator, Mattermost, Sentry and more are coming from the open-source world.

We believe it is imperative for us to also contribute to the communities when we have the possibility. We will maintain this plugin as long as we can, feel free to fork it, do pull requests, emit issues and inform us about the features that are lacking.

Sentry is an excellent solution to detect and manage issues arising from our code; anyone can expand its capabilities thanks to a system of plugins. Lamentably, there is no good documentation about how to create them.

Most people end up cloning an already existing plugin and customizing it for their needs. The sentry-mattermost plugin is originally a copy of the Slack one.

We desire to share the knowledge that we have collected while repairing it. In this part, we are going to talk only about the notification plugins.

The minimal structure is:

plugin_name
|- plugin.py
setup.py

The setup.py:

from setuptools import setup

setup(
    name="plugin_name",
    version="version_number",
    author="author",
    author_email="author email",
    description=("description"),
    license="MIT",
    keywords="keywords",
    url="package url",
    packages=['plugin_name'],
    entry_points={
    'sentry.plugins': [
            'main_class_name = plugin_name.plugin:MainClassName'
        ],
    },

Sentry is going to install and initiate the plugin thanks to the entrypoint. It also means that every time we install a plugin we will need to restart the Sentry.

In the plugin.py file.

You will need to import the base classes.

from sentry.plugins.bases import notify
from sentry_plugins.base import CorePluginMixin
from sentry.integrations import FeatureDescription, IntegrationFeatures

The class notify, contains "NotificationPlugin" which is the base of any notification plugin. It contains a boilerplate with many functions that allow to avoid duplicated issues to be posted, the retrieval of correct parameters and the handling of errors. It saves a lot of time by avoiding us to write a lot of code.

We need then to create a class: (It needs to be the same name as the one put in setup.py)

class MainClassName(CorePluginMixin, notify.NotificationPlugin):
    title = 'plugin_name'
    slug = 'plugin_name'
    description = 'description'
    version = 'version'
    author = 'author'
    author_url = 'author_url'
    required_field = "required_field"
    conf_key = "conf_key"

    feature_descriptions = [
        FeatureDescription(
            """
            Features.  
            """,
            IntegrationFeatures.ALERT_RULE,
        )
    ]

As we may see, there is much redundant information with the setup.py. It is possible for the sake of simplicity, to create another Python file containing all that constant and import it in both setup.py and plugin.py.

In these attributes, the conf_key must be unique. It allows Sentry to spot the projects for which we have configured the plugin; without it, it will always display that there has been no configuration. The easiest solution is to name it like the slug of the plugin.

Generating a configuration, form is simple, we create a function in the class:

def get_config(self, project, **kwargs):
    return [
        {
            "name": "webhook",
            "label": "Webhook URL",
            "type": "url",
            "placeholder": "e.g. https://mattermost.example.com/hooks/00000000000000000",
            "required": True,
            "help": "Your custom mattermost webhook URL.",
        },
        {
            "name": "include_rules",
            "label": "Include Rules",
            "type": "bool",
            "required": False,
            "help": "Include rules with notifications.",
        },
        {
            "name": "include_tags",
            "label": "Include tags",
            "type": "bool",
            "required": False,
            "help": "Include tags with notifications."
        }]

There are many types: url, bool and string. You can add as many as you want. We believe the attributes are self-explanatory. It is important to note that the name, is the name of the attribute used to save the data in our project object (for future use).

In any function of the class, you can access a configuration attribute like this:

self.get_option('name', project)

Another class function is needed to tell Sentry how to identify a project that has been correctly configured.

def is_configured(self, project):
    return bool(self.get_option("name", project))

The goal of this function is to check all the required fields of the config. If there are many required fields it is possible to use:

return all((self.get_option(k, project) for k in ("name1',"name2",)))

There are several plugins that overload the notify function. It is a bad idea. As it appears the issue of the plugin was coming from the fact that notify function parameters changed.

This function is calling notify_users where we need to put our code which will contact our other service.

def notify_users(self, group, event, triggering_rules, fail_silently=False, **kwargs):
    project = event.group.project
    if not self.is_configured(project):
        return

    payload = prepare_payload(self, event, triggering_rules)
    # Your code to contact your service.
    return request(webhook, payload)

Here it is up to the creator to make the necessary. All operations should be there. If we need to call an API (or open a socket, or write a file, etc.) we will do it in this function.

The event object contains many of the information you may want to represent:

Group object:

group = event.group

Project object:

project = group.project

Project name:

project_name = event.project.get_full_name().encode("utf-8")

Issue title:

title = event.group.message_short.encode('utf-8')

Link to the issue:

link_to_issue = event.group.get_absolute_url()

Error message:

culprit = event.group.culprit.encode("utf-8")

And now we reach the end of this blog post. I hope it has been useful, and that it will have inspired you to create sentry integrations for the services you use. If you have any questions, do not hesitate to ask them in the comment section.

Have a lovely day & happy hacking!

By PXke
the 02/10/2020 tags: sentry, python, mattermost, plugin, notification, opensource, github, development, Updated: 02/10/2020