├── alertmanager-notifier ├── lib │ ├── __init__.py │ ├── constants.py │ ├── log.py │ ├── utils.py │ └── notifiers.py ├── requirements.txt ├── __init__.py └── alertmanager-notifier.py ├── alertmanager-notifier.sh ├── .gitlab-ci.yml ├── templates ├── too_long.html.j2 ├── text.j2 ├── markdown.md.j2 └── html.j2 ├── Dockerfile ├── LICENSE ├── .gitignore ├── README.md └── .pylintrc /alertmanager-notifier/lib/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /alertmanager-notifier/requirements.txt: -------------------------------------------------------------------------------- 1 | pygelf==0.3.6 2 | ix_notifiers==0.0.222843990 3 | waitress==1.4.4 4 | flask 5 | -------------------------------------------------------------------------------- /alertmanager-notifier.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | exec python3 -m "alertmanager-notifier.alertmanager-notifier" "$@" 4 | -------------------------------------------------------------------------------- /alertmanager-notifier/lib/constants.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ Constants declarations """ 4 | 5 | VERSION = None 6 | BUILD = None 7 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | DOCKERHUB_REPO_NAME: alertmanager-notifier 3 | ENABLE_ARM64: 'true' 4 | ENABLE_ARMv7: 'true' 5 | ENABLE_ARMv6: 'true' 6 | 7 | include: 8 | - project: 'ix.ai/ci-templates' 9 | file: '/python-project.yml' 10 | -------------------------------------------------------------------------------- /templates/too_long.html.j2: -------------------------------------------------------------------------------- 1 | {% if title -%} 2 |
{{ title }}
3 | {%- endif %} 4 | WARNING!: The full alert list cannot be sent, since it is larger ({{ current_length }} characters) than the maximum message limit for Telegram (4096 characters). Check the logs for details or check out the alerts on Alertmanager. 5 | -------------------------------------------------------------------------------- /alertmanager-notifier/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ initializes alertmanager_notifier """ 4 | 5 | import os 6 | from .lib import log as logging 7 | 8 | log = logging.setup_logger( 9 | name=__package__, 10 | level=os.environ.get('LOGLEVEL', 'INFO'), 11 | gelf_host=os.environ.get('GELF_HOST'), 12 | gelf_port=int(os.environ.get('GELF_PORT', 12201)), 13 | _ix_id=os.environ.get(__package__), 14 | ) 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | LABEL maintainer="docker@ix.ai" \ 3 | ai.ix.repository="ix.ai/alertmanager-notifier" 4 | 5 | COPY alertmanager-notifier/requirements.txt /alertmanager-notifier/requirements.txt 6 | 7 | RUN apk --no-cache upgrade && \ 8 | apk --no-cache add python3 py3-pip py3-waitress py3-flask py3-cryptography && \ 9 | pip3 install --no-cache-dir -r /alertmanager-notifier/requirements.txt 10 | 11 | COPY alertmanager-notifier/ /alertmanager-notifier 12 | COPY templates/ /templates 13 | COPY alertmanager-notifier.sh /usr/local/bin/alertmanager-notifier.sh 14 | 15 | EXPOSE 9119 16 | 17 | ENTRYPOINT ["/usr/local/bin/alertmanager-notifier.sh"] 18 | -------------------------------------------------------------------------------- /templates/text.j2: -------------------------------------------------------------------------------- 1 | {% if title -%} 2 | # {{ title }} 3 | {%- endif %} 4 | {%- for alert in alerts %} 5 | 6 | {% if alert['status'] == 'firing' %}🔥{% else %}✅{% endif %}{{ alert['status']|upper }}{% if alert['status'] == 'firing' %}🔥{% else %}✅{% endif %} 7 | {% for annotation in alert['annotations'] | reverse -%} 8 | {{ annotation }}: {{ alert['annotations'][annotation] }} 9 | {% endfor %} 10 | 11 | {% if not exclude_labels -%} 12 | {% for label in alert['labels'] -%} 13 | {{ label }}: {{ alert['labels'][label] }} 14 | {% endfor %} 15 | {%- endif %} 16 | Since: {{ alert['startsAt'] }} 17 | {% if alert['status'] != 'firing' %}Ended: {{ alert['endsAt'] }} 18 | {% endif %}Generator: {{ alert['generatorURL'] }} 19 | {%- endfor %} 20 | Alertmanager URL: {{ external_url }}/#/alerts?receiver={{ receiver | urlencode }} 21 | -------------------------------------------------------------------------------- /templates/markdown.md.j2: -------------------------------------------------------------------------------- 1 | {% if title -%} 2 | **{{ title }}** 3 | {% endif %} 4 | {% for alert in alerts %} 5 | 6 | {% if alert['status'] == 'firing' %}🔥{% else %}✅{% endif %}**{{ alert['status']|upper }}**{% if alert['status'] == 'firing' %}🔥{% else %}✅{% endif %} 7 | 8 | {% for annotation in alert['annotations'] | reverse -%} 9 | **{{ annotation | safe }}**: `{{ alert['annotations'][annotation] | safe }}` 10 | {% endfor %} 11 | 12 | {% if not exclude_labels -%} 13 | {% for label in alert['labels'] -%} 14 | **{{ label | safe }}**: {{ alert['labels'][label] | safe }} 15 | {% endfor -%} 16 | 17 | {%- endif %} 18 | **Since**: `{{ alert['startsAt'] }}` 19 | {% if alert['status'] != 'firing' %}**Ended:** `{{ alert['endsAt'] }}` 20 | {% endif %}**Generator**: [Prometheus Query]({{ alert['generatorURL'] }}) 21 | {% endfor -%} 22 | **Alertmanager URL**: [View Alerts on Alertmanager]({{ external_url }}/#/alerts?receiver={{ receiver | urlencode }}) 23 | -------------------------------------------------------------------------------- /templates/html.j2: -------------------------------------------------------------------------------- 1 | {% if title -%} 2 |
{{ title }}
3 | {%- endif %} 4 | {%- for alert in alerts %} 5 | {% if alert['status'] == 'firing' %}🔥{% else %}✅{% endif %}{{ alert['status']|upper }}{% if alert['status'] == 'firing' %}🔥{% else %}✅{% endif %} 6 | {% for annotation in alert['annotations'] | reverse -%} 7 | {{ annotation | safe }}: {{ alert['annotations'][annotation] | safe }} 8 | {% endfor %} 9 | {% if not exclude_labels -%} 10 | {% for label in alert['labels'] -%} 11 | {{ label | safe }}: {{ alert['labels'][label] | safe }} 12 | {% endfor %} 13 | {% endif -%} 14 | Since: {{ alert['startsAt'] }} 15 | {% if alert['status'] != 'firing' %}Ended: {{ alert['endsAt'] }} 16 | {% endif %}Generator: Prometheus Query 17 | {% endfor -%} 18 | Alertmanager URL: View Alerts on Alertmanager 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019 ix.ai, https://ix.ai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /alertmanager-notifier/lib/log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ Global logging configuration """ 4 | 5 | import logging 6 | import pygelf 7 | 8 | 9 | def setup_logger(name=__package__, level='INFO', gelf_host=None, gelf_port=None, **kwargs): 10 | """ sets up the logger """ 11 | logging.basicConfig(handlers=[logging.NullHandler()]) 12 | formatter = logging.Formatter( 13 | fmt='%(asctime)s.%(msecs)03d %(levelname)s [%(module)s.%(funcName)s] %(message)s', 14 | datefmt='%Y-%m-%d %H:%M:%S', 15 | ) 16 | logger = logging.getLogger(name) 17 | logger.setLevel(level) 18 | 19 | handler = logging.StreamHandler() 20 | handler.setFormatter(formatter) 21 | logger.addHandler(handler) 22 | 23 | if gelf_host and gelf_port: 24 | handler = pygelf.GelfUdpHandler( 25 | host=gelf_host, 26 | port=gelf_port, 27 | debug=True, 28 | include_extra_fields=True, 29 | **kwargs 30 | ) 31 | logger.addHandler(handler) 32 | 33 | ix_logger = logging.getLogger('ix_notifiers') 34 | ix_logger.setLevel(level) 35 | ix_logger.addHandler(handler) 36 | 37 | return logger 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | share/python-wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .dmypy.json 111 | dmypy.json 112 | 113 | # Pyre type checker 114 | .pyre/ 115 | -------------------------------------------------------------------------------- /alertmanager-notifier/lib/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ Various utilities """ 4 | 5 | import logging 6 | from distutils.util import strtobool 7 | from flask import render_template 8 | 9 | log = logging.getLogger(__package__) 10 | 11 | 12 | def redact(params: dict, settings: dict, message: str) -> str: 13 | """ 14 | based on params and the values of settings, it replaces sensitive 15 | information in message with a redacted string 16 | """ 17 | for param, setting in params.items(): 18 | if setting.get('redact') and settings.get(param): 19 | message = str(message).replace(settings.get(param), 'xxxREDACTEDxxx') 20 | return message 21 | 22 | 23 | def convert_type(param: str, target: str): 24 | """ 25 | converts string param to type target 26 | """ 27 | converted = param 28 | if target == "boolean": 29 | converted = bool(strtobool(param)) 30 | if target == "integer": 31 | converted = int(param) 32 | return converted 33 | 34 | 35 | def template_message(include_title=False, template='markdown.md.j2', exclude_labels=True, current_length=0, **kwargs): 36 | """ 37 | Formats the alerts for markdown notifiers 38 | 39 | Use `include_title` to specify if the title should be included in the message. 40 | If it's set to `False`, a separate key `title` will be returned. 41 | 42 | @return: False if the message processing fails otherwise dict 43 | """ 44 | processed = {'message': ''} 45 | alerts_count = len(kwargs['alerts']) 46 | title = f"{alerts_count} alert(s) received" 47 | if not include_title: 48 | processed.update({'title': f"{title}"}) 49 | title = None 50 | processed['message'] = render_template( 51 | template, 52 | title=title, 53 | alerts=kwargs['alerts'], 54 | external_url=kwargs['external_url'], 55 | receiver=kwargs['receiver'], 56 | exclude_labels=exclude_labels, 57 | current_length=current_length, 58 | ) 59 | for alert in kwargs['alerts']: 60 | if int(alert['annotations'].get('priority', -1)) > processed.get('priority', -1): 61 | processed['priority'] = int(alert['annotations']['priority']) 62 | return processed 63 | -------------------------------------------------------------------------------- /alertmanager-notifier/lib/notifiers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ notification core """ 4 | 5 | import logging 6 | from ix_notifiers.core import IxNotifiers 7 | from .utils import template_message 8 | 9 | log = logging.getLogger(__package__) 10 | 11 | 12 | def start(**kwargs): 13 | """ Returns an instance of Notify() """ 14 | n = Notify() 15 | notifiers = kwargs.get('notifiers', []) 16 | if not len(notifiers) > 0: 17 | notifiers = ['null'] 18 | for notifier in notifiers: 19 | notifier_settings = {} 20 | for k, v in kwargs.items(): 21 | # Ensures that only the settings for this notifier are passed 22 | if k.split('_')[0] == notifier: 23 | notifier_settings.update({k: v}) 24 | # Common variables 25 | notifier_settings.update({'exclude_labels': kwargs['exclude_labels']}) 26 | n.register(notifier, **notifier_settings) 27 | return Notify(**kwargs) 28 | 29 | 30 | class Notify(IxNotifiers): 31 | """ the Notify class """ 32 | 33 | def __init__(self, **kwargs): 34 | for variable, value in kwargs.items(): 35 | setattr(self, variable, value) 36 | super().__init__() 37 | 38 | def notify(self, **kwargs): 39 | """ dispatches a notification to the registered notifiers """ 40 | success = ('All notification channels failed', 500) 41 | for notifier_name, notifier in self.registered.items(): 42 | log.debug(f'Sending notification to {notifier_name}') 43 | notification_method = self.__getattribute__(f'{notifier_name}_notify') 44 | if notification_method(notifier=notifier, **kwargs): 45 | success = ('OK', 200) 46 | return success 47 | 48 | def gotify_notify(self, notifier, **kwargs): 49 | """ parses the arguments, formats the message and dispatches it """ 50 | # pylint: disable=no-member 51 | log.debug('Sending message to gotify') 52 | processed_alerts = template_message( 53 | alerts=kwargs['alerts'], 54 | external_url=kwargs['externalURL'], 55 | receiver=kwargs['receiver'], 56 | include_title=False, 57 | template=self.gotify_template, 58 | exclude_labels=self.exclude_labels, 59 | ) 60 | return notifier.send(**processed_alerts) 61 | 62 | def telegram_notify(self, notifier, **kwargs): 63 | """ parses the arguments, formats the message and dispatches it """ 64 | # pylint: disable=no-member 65 | log.debug('Sending message to telegram') 66 | processed_alerts = template_message( 67 | alerts=kwargs['alerts'], 68 | external_url=kwargs['externalURL'], 69 | receiver=kwargs['receiver'], 70 | include_title=True, 71 | template=self.telegram_template, 72 | exclude_labels=self.exclude_labels, 73 | ) 74 | msg_len = len(processed_alerts['message']) 75 | if msg_len > 4096: 76 | log.warning(f"The message is too long ({msg_len}>4096)") 77 | processed_alerts = template_message( 78 | alerts=kwargs['alerts'], 79 | external_url=kwargs['externalURL'], 80 | receiver=kwargs['receiver'], 81 | include_title=True, 82 | template=self.telegram_template_too_long, 83 | current_length=msg_len, 84 | ) 85 | processed_alerts.update({'parse_mode': 'HTML'}) 86 | notification_return = notifier.send(**processed_alerts) 87 | log.debug(notification_return) 88 | return notification_return 89 | 90 | def null_notify(self, notifier, **kwargs): 91 | """ dispatches directly """ 92 | # pylint: disable=no-member 93 | log.debug('Sending message to null') 94 | processed_alerts = template_message( 95 | alerts=kwargs['alerts'], 96 | external_url=kwargs['externalURL'], 97 | receiver=kwargs['receiver'], 98 | include_title=True, 99 | template=self.null_template, 100 | exclude_labels=self.exclude_labels, 101 | ) 102 | return notifier.send(**processed_alerts) 103 | -------------------------------------------------------------------------------- /alertmanager-notifier/alertmanager-notifier.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ Web server that translates alertmanager alerts into telegram messages """ 3 | 4 | import logging 5 | import os 6 | from waitress import serve 7 | import telegram 8 | from flask import Flask 9 | from flask import request 10 | from .lib import constants 11 | from .lib import notifiers 12 | from .lib.utils import redact 13 | from .lib.utils import convert_type 14 | 15 | a = Flask(__name__, template_folder='../templates') 16 | a.secret_key = os.urandom(64).hex() 17 | 18 | log = logging.getLogger(__package__) 19 | version = f'{constants.VERSION}-{constants.BUILD}' 20 | 21 | 22 | @a.route('/alert', methods=['POST']) 23 | def parse_request(): 24 | """ Receives the alert and sends the notification """ 25 | content = request.get_json() 26 | 27 | return_message = "" 28 | try: 29 | log.info(f"Received {len(content['alerts'])} alert(s).") 30 | log.debug(f'Parsing content: {content}') 31 | return_message = n.notify(**content) 32 | except (KeyError, TypeError) as e: 33 | message = ( 34 | 'Make sure that `Content-Type: application/json` is set and that the key `alerts` exists.' 35 | f'The exception: {e}' 36 | ) 37 | log.error(message) 38 | return_message = (message, 400) 39 | 40 | return return_message 41 | 42 | 43 | @a.route('/healthz') 44 | def healthz(): 45 | """ Healthcheck """ 46 | return (f'{__package__} {version}', 200) 47 | 48 | 49 | def startup(): 50 | """ Starts everything up """ 51 | params = { 52 | 'telegram_token': { 53 | 'type': 'string', 54 | 'redact': True, 55 | }, 56 | 'telegram_chat_id': { 57 | 'type': 'string', 58 | }, 59 | 'telegram_always_succeed': { 60 | 'type': 'boolean', 61 | 'default': 'no', 62 | }, 63 | 'telegram_max_retries': { 64 | 'type': 'integer', 65 | 'default': '0', 66 | }, 67 | 'gotify_url': { 68 | 'type': 'string', 69 | }, 70 | 'gotify_token': { 71 | 'type': 'string', 72 | 'redact': True, 73 | }, 74 | 'port': { 75 | 'type': 'integer', 76 | 'default': '8899', 77 | }, 78 | 'address': { 79 | 'type': 'string', 80 | 'default': '*', 81 | }, 82 | 'telegram_template': { 83 | 'type': 'string', 84 | 'default': 'html.j2', 85 | }, 86 | 'telegram_template_too_long': { 87 | 'type': 'string', 88 | 'default': 'too_long.html.j2', 89 | }, 90 | 'gotify_template': { 91 | 'type': 'string', 92 | 'default': 'markdown.md.j2', 93 | }, 94 | 'null_template': { 95 | 'type': 'string', 96 | 'default': 'text.j2', 97 | }, 98 | 'exclude_labels': { 99 | 'type': 'boolean', 100 | 'default': 'yes', 101 | } 102 | } 103 | 104 | settings = { 105 | 'notifiers': [], 106 | } 107 | 108 | for param, param_settings in params.items(): 109 | try: 110 | settings.update({param: convert_type(os.environ[param.upper()], param_settings['type'])}) 111 | except ValueError: # Wrong value for the environment variable 112 | log.warning(f"`{os.environ[param.upper()]}` not understood for {param.upper()}. Ignoring.") 113 | except KeyError: # No environment variable set for the param 114 | pass 115 | 116 | try: 117 | if param not in settings: 118 | settings[param] = convert_type(param_settings['default'], param_settings['type']) 119 | log.info(f'{param.upper()} is set to `{redact(params, settings, settings[param])}`') 120 | except KeyError: # No default value for the param 121 | pass 122 | 123 | try: 124 | if settings['telegram_token'] and settings['telegram_chat_id']: 125 | settings['notifiers'].append('telegram') 126 | except KeyError: 127 | pass 128 | 129 | try: 130 | if settings['gotify_url'] and settings['gotify_token']: 131 | settings['notifiers'].append('gotify') 132 | except KeyError: 133 | pass 134 | 135 | log.info(f"Starting {__package__} {version}, listening on {settings['address']}:{settings['port']}") 136 | return settings 137 | 138 | 139 | if __name__ == '__main__': 140 | options = startup() 141 | try: 142 | if not options['notifiers']: 143 | log.warning('No notifier configured. Using `null`') 144 | n = notifiers.start(**options) 145 | except (ValueError, telegram.error.InvalidToken) as error: 146 | log.error(error) 147 | else: 148 | serve(a, host=options['address'], port=options['port'], ident=f'{__package__} {version}') 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alertmanager-notifier 2 | 3 | [![Pipeline Status](https://gitlab.com/ix.ai/alertmanager-notifier/badges/master/pipeline.svg)](https://gitlab.com/ix.ai/alertmanager-notifier/) 4 | [![Docker Stars](https://img.shields.io/docker/stars/ixdotai/alertmanager-notifier.svg)](https://hub.docker.com/r/ixdotai/alertmanager-notifier/) 5 | [![Docker Pulls](https://img.shields.io/docker/pulls/ixdotai/alertmanager-notifier.svg)](https://hub.docker.com/r/ixdotai/alertmanager-notifier/) 6 | [![Gitlab Project](https://img.shields.io/badge/GitLab-Project-554488.svg)](https://gitlab.com/ix.ai/alertmanager-notifier/) 7 | 8 | A notifier for [alertmanager](https://github.com/prometheus/alertmanager), written in python. It supports multiple notification channels and new ones can be easily added. 9 | 10 | ## Running a simple test: 11 | ```sh 12 | docker run --rm -it \ 13 | -p 8899:8899 \ 14 | -e TELEGRAM_TOKEN="your token" \ 15 | -e TELEGRAM_CHAT_ID="your chat id" \ 16 | -e GOTIFY_URL="https://gotify" \ 17 | -e GOTIFY_TOKEN="your gotify token" \ 18 | -e EXCLUDE_LABELS="yes" \ 19 | --name alertmanager-notifier \ 20 | registry.gitlab.com/ix.ai/alertmanager-notifier:latest 21 | ``` 22 | 23 | Run the test agains the bot: 24 | ```sh 25 | curl -X POST -d '{"externalURL": "http://foo.bar/", "receiver": "alertmanager-notifier-webhook", "alerts": [{"status":"Testing alertmanager-notifier", "labels":{}, "annotations":{}, "generatorURL": "http://foo.bar"}]}' -H "Content-Type: application/json" localhost:8899/alert 26 | ``` 27 | 28 | ## Configure alertmanager: 29 | ```yml 30 | route: 31 | receiver: 'alertmanager webhook' 32 | routes: 33 | - receiver: 'alertmanager-notifier-webhook' 34 | 35 | receivers: 36 | - name: 'alertmanager-notifier-webhook' 37 | webhook_configs: 38 | - url: http://alertmanager-notifier:8899/alert 39 | ``` 40 | 41 | ## Supported environment variables: 42 | 43 | | **Variable** | **Default** | **Description** | 44 | |:--------------------|:----------------:|:---------------------------------------------------------------------------------------------------------------------------| 45 | | `TELEGRAM_TOKEN` | - | see the [Telegram documentation](https://core.telegram.org/bots#creating-a-new-bot) how to get a new token | 46 | | `TELEGRAM_CHAT_ID` | - | see this question on [stackoverflow](https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id) | 47 | | `TELEGRAM_TEMPLATE` | `html.j2` | allows you to specify another (HTML) template, in case you've mounted it under `/templates` | 48 | | `TELEGRAM_TEMPLATE_TOO_LONG` | `too_long.html.j2` | allows you to specify another (HTML) template for Telegram, for the case that the message size exceeds the maximum message size (4096 characters) | 49 | | `TELEGRAM_MAX_RETRIES` | `0` | The maximum number of times an alert should be tried to be sent out (`0` for unlimited) | 50 | | `TELEGRAM_ALWAYS_SUCCEED` | `no` | Set this variable to `yes` so that alertmanager-notifier always sends a `200 OK` to alertmanager, even if the Telegram notification wasn't successful | 51 | | `GOTIFY_URL` | - | the URL of the [Gotify](https://gotify.net/) server | 52 | | `GOTIFY_TOKEN` | - | the APP token for Gotify | 53 | | `GOTIFY_TEMPLATE` | `markdown.md.j2` | allows you to specify another (HTML) template, in case you've mounted it under `/templates` | 54 | | `EXCLUDE_LABELS` | `yes` | set this to `no` to include the labels from the notifications | 55 | | `LOGLEVEL` | `INFO` | [Logging Level](https://docs.python.org/3/library/logging.html#levels) | 56 | | `GELF_HOST` | - | If set, the exporter will also log to this [GELF](https://docs.graylog.org/en/3.0/pages/gelf.html) capable host on UDP | 57 | | `GELF_PORT` | `12201` | Ignored, if `GELF_HOST` is unset. The UDP port for GELF logging | 58 | | `PORT` | `8899` | the port for incoming connections | 59 | | `ADDRESS` | `*` | the address for the bot to listen on | 60 | 61 | **NOTE**: If no notifier is configured, the `Null` notifier will be used and the notification will only be logged 62 | 63 | ## Gotify Priority 64 | 65 | Gotify supports message priorities, that are also mapped to Android Importance (see [gotify/android#18](https://github.com/gotify/android/issues/18)). 66 | 67 | If you set the *annotation* `priority` to your alert, with a number as value, this will be passed through to gotify. 68 | 69 | **Note**: Since alertmanager supports sending multiple alerts in one message, alertmanager-notifier will always use the **highest** priority value for gotify from the batch. 70 | 71 | ## Templating 72 | 73 | **alertmanager-notifier** supports jinja templating. take a look in the [templates/](templates/) folder for examples for that. If you want to use your own template, mount it as a volume in docker and set the `*_TEMPLATE` environment variable. The mount path should be under `/templates/` (for example `/templates/my-amazing-template`). 74 | 75 | ## Tags and Arch 76 | 77 | The images are multi-arch, with builds for amd64, arm64, armv7 and armv6. 78 | * `vN.N.N` - for example v0.0.1 79 | * `latest` - always pointing to the latest version 80 | * `dev-master` - the last build on the master branch 81 | 82 | ### Images: 83 | * Docker Hub: `ixdotai/alertmanager-notifier` 84 | * Gitlab Registry: `registry.gitlab.com/ix.ai/alertmanager-notifier` 85 | 86 | ## Resources: 87 | * GitLab: https://gitlab.com/ix.ai/alertmanager-notifier 88 | * GitHub: https://github.com/ix-ai/alertmanager-notifier 89 | * Docker Hub: https://hub.docker.com/r/ixdotai/alertmanager-notifier 90 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 21 | # number of processors available to use. 22 | jobs=1 23 | 24 | # Control the amount of potential inferred values when inferring a single 25 | # object. This can help the performance when dealing with large functions or 26 | # complex, nested conditions. 27 | limit-inference-results=100 28 | 29 | # List of plugins (as comma separated values of python modules names) to load, 30 | # usually to register additional checkers. 31 | load-plugins= 32 | 33 | # Pickle collected data for later comparisons. 34 | persistent=yes 35 | 36 | # Specify a configuration file. 37 | #rcfile= 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=logging-fstring-interpolation, 64 | # too-few-public-methods, 65 | invalid-name, 66 | no-self-use, 67 | 68 | # Enable the message, report, category or checker with the given id(s). You can 69 | # either give multiple identifier separated by comma (,) or put this option 70 | # multiple time (only on the command line, not in the configuration file where 71 | # it should appear only once). See also the "--disable" option for examples. 72 | enable=c-extension-no-member 73 | 74 | 75 | [REPORTS] 76 | 77 | # Python expression which should return a note less than 10 (10 is the highest 78 | # note). You have access to the variables errors warning, statement which 79 | # respectively contain the number of errors / warnings messages and the total 80 | # number of statements analyzed. This is used by the global evaluation report 81 | # (RP0004). 82 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 83 | 84 | # Template used to display messages. This is a python new-style format string 85 | # used to format the message information. See doc for all details. 86 | #msg-template= 87 | 88 | # Set the output format. Available formats are text, parseable, colorized, json 89 | # and msvs (visual studio). You can also give a reporter class, e.g. 90 | # mypackage.mymodule.MyReporterClass. 91 | output-format=text 92 | 93 | # Tells whether to display a full report or only the messages. 94 | reports=no 95 | 96 | # Activate the evaluation score. 97 | score=yes 98 | 99 | 100 | [REFACTORING] 101 | 102 | # Maximum number of nested blocks for function / method body 103 | max-nested-blocks=5 104 | 105 | # Complete name of functions that never returns. When checking for 106 | # inconsistent-return-statements if a never returning function is called then 107 | # it will be considered as an explicit return statement and no message will be 108 | # printed. 109 | never-returning-functions=sys.exit 110 | 111 | 112 | [LOGGING] 113 | 114 | # Format style used to check logging format string. `old` means using % 115 | # formatting, while `new` is for `{}` formatting. 116 | logging-format-style=new 117 | 118 | # Logging modules to check that the string format arguments are in logging 119 | # function parameter format. 120 | logging-modules=logging 121 | 122 | 123 | [SPELLING] 124 | 125 | # Limits count of emitted suggestions for spelling mistakes. 126 | max-spelling-suggestions=4 127 | 128 | # Spelling dictionary name. Available dictionaries: none. To make it working 129 | # install python-enchant package.. 130 | spelling-dict= 131 | 132 | # List of comma separated words that should not be checked. 133 | spelling-ignore-words= 134 | 135 | # A path to a file that contains private dictionary; one word per line. 136 | spelling-private-dict-file= 137 | 138 | # Tells whether to store unknown words to indicated private dictionary in 139 | # --spelling-private-dict-file option instead of raising a message. 140 | spelling-store-unknown-words=no 141 | 142 | 143 | [MISCELLANEOUS] 144 | 145 | # List of note tags to take in consideration, separated by a comma. 146 | notes=FIXME, 147 | XXX, 148 | TODO 149 | 150 | 151 | [TYPECHECK] 152 | 153 | # List of decorators that produce context managers, such as 154 | # contextlib.contextmanager. Add to this list to register other decorators that 155 | # produce valid context managers. 156 | contextmanager-decorators=contextlib.contextmanager 157 | 158 | # List of members which are set dynamically and missed by pylint inference 159 | # system, and so shouldn't trigger E1101 when accessed. Python regular 160 | # expressions are accepted. 161 | generated-members= 162 | 163 | # Tells whether missing members accessed in mixin class should be ignored. A 164 | # mixin class is detected if its name ends with "mixin" (case insensitive). 165 | ignore-mixin-members=yes 166 | 167 | # Tells whether to warn about missing members when the owner of the attribute 168 | # is inferred to be None. 169 | ignore-none=yes 170 | 171 | # This flag controls whether pylint should warn about no-member and similar 172 | # checks whenever an opaque object is returned when inferring. The inference 173 | # can return multiple potential results while evaluating a Python object, but 174 | # some branches might not be evaluated, which results in partial inference. In 175 | # that case, it might be useful to still emit no-member and other checks for 176 | # the rest of the inferred objects. 177 | ignore-on-opaque-inference=yes 178 | 179 | # List of class names for which member attributes should not be checked (useful 180 | # for classes with dynamically set attributes). This supports the use of 181 | # qualified names. 182 | ignored-classes=optparse.Values,thread._local,_thread._local 183 | 184 | # List of module names for which member attributes should not be checked 185 | # (useful for modules/projects where namespaces are manipulated during runtime 186 | # and thus existing member attributes cannot be deduced by static analysis. It 187 | # supports qualified module names, as well as Unix pattern matching. 188 | ignored-modules= 189 | 190 | # Show a hint with possible names when a member name was not found. The aspect 191 | # of finding the hint is based on edit distance. 192 | missing-member-hint=yes 193 | 194 | # The minimum edit distance a name should have in order to be considered a 195 | # similar match for a missing member name. 196 | missing-member-hint-distance=1 197 | 198 | # The total number of similar names that should be taken in consideration when 199 | # showing a hint for a missing member. 200 | missing-member-max-choices=1 201 | 202 | 203 | [VARIABLES] 204 | 205 | # List of additional names supposed to be defined in builtins. Remember that 206 | # you should avoid defining new builtins when possible. 207 | additional-builtins= 208 | 209 | # Tells whether unused global variables should be treated as a violation. 210 | allow-global-unused-variables=yes 211 | 212 | # List of strings which can identify a callback function by name. A callback 213 | # name must start or end with one of those strings. 214 | callbacks=cb_, 215 | _cb 216 | 217 | # A regular expression matching the name of dummy variables (i.e. expected to 218 | # not be used). 219 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 220 | 221 | # Argument names that match this expression will be ignored. Default to name 222 | # with leading underscore. 223 | ignored-argument-names=_.*|^ignored_|^unused_ 224 | 225 | # Tells whether we should check for unused import in __init__ files. 226 | init-import=no 227 | 228 | # List of qualified module names which can have objects that can redefine 229 | # builtins. 230 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 231 | 232 | 233 | [FORMAT] 234 | 235 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 236 | expected-line-ending-format= 237 | 238 | # Regexp for a line that is allowed to be longer than the limit. 239 | ignore-long-lines=^\s*(# )??$ 240 | 241 | # Number of spaces of indent required inside a hanging or continued line. 242 | indent-after-paren=4 243 | 244 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 245 | # tab). 246 | indent-string=' ' 247 | 248 | # Maximum number of characters on a single line. 249 | max-line-length=120 250 | 251 | # Maximum number of lines in a module. 252 | max-module-lines=1000 253 | 254 | # List of optional constructs for which whitespace checking is disabled. `dict- 255 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 256 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 257 | # `empty-line` allows space-only lines. 258 | no-space-check=trailing-comma, 259 | dict-separator 260 | 261 | # Allow the body of a class to be on the same line as the declaration if body 262 | # contains single statement. 263 | single-line-class-stmt=no 264 | 265 | # Allow the body of an if to be on the same line as the test if there is no 266 | # else. 267 | single-line-if-stmt=no 268 | 269 | 270 | [SIMILARITIES] 271 | 272 | # Ignore comments when computing similarities. 273 | ignore-comments=yes 274 | 275 | # Ignore docstrings when computing similarities. 276 | ignore-docstrings=yes 277 | 278 | # Ignore imports when computing similarities. 279 | ignore-imports=no 280 | 281 | # Minimum lines number of a similarity. 282 | min-similarity-lines=4 283 | 284 | 285 | [BASIC] 286 | 287 | # Naming style matching correct argument names. 288 | argument-naming-style=snake_case 289 | 290 | # Regular expression matching correct argument names. Overrides argument- 291 | # naming-style. 292 | #argument-rgx= 293 | 294 | # Naming style matching correct attribute names. 295 | attr-naming-style=snake_case 296 | 297 | # Regular expression matching correct attribute names. Overrides attr-naming- 298 | # style. 299 | #attr-rgx= 300 | 301 | # Bad variable names which should always be refused, separated by a comma. 302 | bad-names=foo, 303 | bar, 304 | baz, 305 | toto, 306 | tutu, 307 | tata 308 | 309 | # Naming style matching correct class attribute names. 310 | class-attribute-naming-style=any 311 | 312 | # Regular expression matching correct class attribute names. Overrides class- 313 | # attribute-naming-style. 314 | #class-attribute-rgx= 315 | 316 | # Naming style matching correct class names. 317 | class-naming-style=PascalCase 318 | 319 | # Regular expression matching correct class names. Overrides class-naming- 320 | # style. 321 | #class-rgx= 322 | 323 | # Naming style matching correct constant names. 324 | const-naming-style=UPPER_CASE 325 | 326 | # Regular expression matching correct constant names. Overrides const-naming- 327 | # style. 328 | #const-rgx= 329 | 330 | # Minimum line length for functions/classes that require docstrings, shorter 331 | # ones are exempt. 332 | docstring-min-length=-1 333 | 334 | # Naming style matching correct function names. 335 | function-naming-style=snake_case 336 | 337 | # Regular expression matching correct function names. Overrides function- 338 | # naming-style. 339 | #function-rgx= 340 | 341 | # Good variable names which should always be accepted, separated by a comma. 342 | good-names=i, 343 | j, 344 | k, 345 | ex, 346 | Run, 347 | _ 348 | 349 | # Include a hint for the correct naming format with invalid-name. 350 | include-naming-hint=no 351 | 352 | # Naming style matching correct inline iteration names. 353 | inlinevar-naming-style=any 354 | 355 | # Regular expression matching correct inline iteration names. Overrides 356 | # inlinevar-naming-style. 357 | #inlinevar-rgx= 358 | 359 | # Naming style matching correct method names. 360 | method-naming-style=snake_case 361 | 362 | # Regular expression matching correct method names. Overrides method-naming- 363 | # style. 364 | #method-rgx= 365 | 366 | # Naming style matching correct module names. 367 | module-naming-style=any 368 | 369 | # Regular expression matching correct module names. Overrides module-naming- 370 | # style. 371 | #module-rgx= 372 | 373 | # Colon-delimited sets of names that determine each other's naming style when 374 | # the name regexes allow several styles. 375 | name-group= 376 | 377 | # Regular expression which should only match function or class names that do 378 | # not require a docstring. 379 | no-docstring-rgx=^_ 380 | 381 | # List of decorators that produce properties, such as abc.abstractproperty. Add 382 | # to this list to register other decorators that produce valid properties. 383 | # These decorators are taken in consideration only for invalid-name. 384 | property-classes=abc.abstractproperty 385 | 386 | # Naming style matching correct variable names. 387 | variable-naming-style=snake_case 388 | 389 | # Regular expression matching correct variable names. Overrides variable- 390 | # naming-style. 391 | #variable-rgx= 392 | 393 | 394 | [STRING] 395 | 396 | # This flag controls whether the implicit-str-concat-in-sequence should 397 | # generate a warning on implicit string concatenation in sequences defined over 398 | # several lines. 399 | check-str-concat-over-line-jumps=no 400 | 401 | 402 | [IMPORTS] 403 | 404 | # Allow wildcard imports from modules that define __all__. 405 | allow-wildcard-with-all=no 406 | 407 | # Analyse import fallback blocks. This can be used to support both Python 2 and 408 | # 3 compatible code, which means that the block might have code that exists 409 | # only in one or another interpreter, leading to false positives when analysed. 410 | analyse-fallback-blocks=no 411 | 412 | # Deprecated modules which should not be used, separated by a comma. 413 | deprecated-modules=optparse,tkinter.tix 414 | 415 | # Create a graph of external dependencies in the given file (report RP0402 must 416 | # not be disabled). 417 | ext-import-graph= 418 | 419 | # Create a graph of every (i.e. internal and external) dependencies in the 420 | # given file (report RP0402 must not be disabled). 421 | import-graph= 422 | 423 | # Create a graph of internal dependencies in the given file (report RP0402 must 424 | # not be disabled). 425 | int-import-graph= 426 | 427 | # Force import order to recognize a module as part of the standard 428 | # compatibility libraries. 429 | known-standard-library= 430 | 431 | # Force import order to recognize a module as part of a third party library. 432 | known-third-party=enchant 433 | 434 | 435 | [CLASSES] 436 | 437 | # List of method names used to declare (i.e. assign) instance attributes. 438 | defining-attr-methods=__init__, 439 | __new__, 440 | setUp 441 | 442 | # List of member names, which should be excluded from the protected access 443 | # warning. 444 | exclude-protected=_asdict, 445 | _fields, 446 | _replace, 447 | _source, 448 | _make 449 | 450 | # List of valid names for the first argument in a class method. 451 | valid-classmethod-first-arg=cls 452 | 453 | # List of valid names for the first argument in a metaclass class method. 454 | valid-metaclass-classmethod-first-arg=cls 455 | 456 | 457 | [DESIGN] 458 | 459 | # Maximum number of arguments for function / method. 460 | max-args=5 461 | 462 | # Maximum number of attributes for a class (see R0902). 463 | max-attributes=7 464 | 465 | # Maximum number of boolean expressions in an if statement. 466 | max-bool-expr=5 467 | 468 | # Maximum number of branch for function / method body. 469 | max-branches=12 470 | 471 | # Maximum number of locals for function / method body. 472 | max-locals=15 473 | 474 | # Maximum number of parents for a class (see R0901). 475 | max-parents=7 476 | 477 | # Maximum number of public methods for a class (see R0904). 478 | max-public-methods=20 479 | 480 | # Maximum number of return / yield for function / method body. 481 | max-returns=6 482 | 483 | # Maximum number of statements in function / method body. 484 | max-statements=50 485 | 486 | # Minimum number of public methods for a class (see R0903). 487 | min-public-methods=2 488 | 489 | 490 | [EXCEPTIONS] 491 | 492 | # Exceptions that will emit a warning when being caught. Defaults to 493 | # "BaseException, Exception". 494 | overgeneral-exceptions=BaseException 495 | --------------------------------------------------------------------------------