├── 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 | [](https://gitlab.com/ix.ai/alertmanager-notifier/)
4 | [](https://hub.docker.com/r/ixdotai/alertmanager-notifier/)
5 | [](https://hub.docker.com/r/ixdotai/alertmanager-notifier/)
6 | [](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 |
--------------------------------------------------------------------------------