├── .gitignore ├── .gitlab-ci.yml ├── .pylintrc ├── Dockerfile ├── LICENSE ├── README.md ├── alertmanager-notifier.sh ├── alertmanager-notifier ├── __init__.py ├── alertmanager-notifier.py ├── lib │ ├── __init__.py │ ├── constants.py │ ├── log.py │ ├── notifiers.py │ └── utils.py └── requirements.txt ├── build.sh ├── renovate.json └── templates ├── html.j2 ├── markdown.md.j2 └── text.j2 /.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 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variables: 3 | DOCKERHUB_REPO_NAME: alertmanager-notifier 4 | GITHUB_REPO_NAME: ix-ai/alertmanager-notifier 5 | ENABLE_ARM64: 'true' 6 | ENABLE_ARMv7: 'true' 7 | ENABLE_ARMv6: 'true' 8 | ENABLE_386: 'true' 9 | 10 | include: 11 | - project: 'egos-tech/pipelines' 12 | file: '/python-project.yml' 13 | -------------------------------------------------------------------------------- /.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 | 67 | # Enable the message, report, category or checker with the given id(s). You can 68 | # either give multiple identifier separated by comma (,) or put this option 69 | # multiple time (only on the command line, not in the configuration file where 70 | # it should appear only once). See also the "--disable" option for examples. 71 | enable=c-extension-no-member 72 | 73 | 74 | [REPORTS] 75 | 76 | # Python expression which should return a note less than 10 (10 is the highest 77 | # note). You have access to the variables errors warning, statement which 78 | # respectively contain the number of errors / warnings messages and the total 79 | # number of statements analyzed. This is used by the global evaluation report 80 | # (RP0004). 81 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 82 | 83 | # Template used to display messages. This is a python new-style format string 84 | # used to format the message information. See doc for all details. 85 | #msg-template= 86 | 87 | # Set the output format. Available formats are text, parseable, colorized, json 88 | # and msvs (visual studio). You can also give a reporter class, e.g. 89 | # mypackage.mymodule.MyReporterClass. 90 | output-format=text 91 | 92 | # Tells whether to display a full report or only the messages. 93 | reports=no 94 | 95 | # Activate the evaluation score. 96 | score=yes 97 | 98 | 99 | [REFACTORING] 100 | 101 | # Maximum number of nested blocks for function / method body 102 | max-nested-blocks=5 103 | 104 | # Complete name of functions that never returns. When checking for 105 | # inconsistent-return-statements if a never returning function is called then 106 | # it will be considered as an explicit return statement and no message will be 107 | # printed. 108 | never-returning-functions=sys.exit 109 | 110 | 111 | [LOGGING] 112 | 113 | # Format style used to check logging format string. `old` means using % 114 | # formatting, while `new` is for `{}` formatting. 115 | logging-format-style=new 116 | 117 | # Logging modules to check that the string format arguments are in logging 118 | # function parameter format. 119 | logging-modules=logging 120 | 121 | 122 | [SPELLING] 123 | 124 | # Limits count of emitted suggestions for spelling mistakes. 125 | max-spelling-suggestions=4 126 | 127 | # Spelling dictionary name. Available dictionaries: none. To make it working 128 | # install python-enchant package.. 129 | spelling-dict= 130 | 131 | # List of comma separated words that should not be checked. 132 | spelling-ignore-words= 133 | 134 | # A path to a file that contains private dictionary; one word per line. 135 | spelling-private-dict-file= 136 | 137 | # Tells whether to store unknown words to indicated private dictionary in 138 | # --spelling-private-dict-file option instead of raising a message. 139 | spelling-store-unknown-words=no 140 | 141 | 142 | [MISCELLANEOUS] 143 | 144 | # List of note tags to take in consideration, separated by a comma. 145 | notes=FIXME, 146 | XXX, 147 | TODO 148 | 149 | 150 | [TYPECHECK] 151 | 152 | # List of decorators that produce context managers, such as 153 | # contextlib.contextmanager. Add to this list to register other decorators that 154 | # produce valid context managers. 155 | contextmanager-decorators=contextlib.contextmanager 156 | 157 | # List of members which are set dynamically and missed by pylint inference 158 | # system, and so shouldn't trigger E1101 when accessed. Python regular 159 | # expressions are accepted. 160 | generated-members= 161 | 162 | # Tells whether missing members accessed in mixin class should be ignored. A 163 | # mixin class is detected if its name ends with "mixin" (case insensitive). 164 | ignore-mixin-members=yes 165 | 166 | # Tells whether to warn about missing members when the owner of the attribute 167 | # is inferred to be None. 168 | ignore-none=yes 169 | 170 | # This flag controls whether pylint should warn about no-member and similar 171 | # checks whenever an opaque object is returned when inferring. The inference 172 | # can return multiple potential results while evaluating a Python object, but 173 | # some branches might not be evaluated, which results in partial inference. In 174 | # that case, it might be useful to still emit no-member and other checks for 175 | # the rest of the inferred objects. 176 | ignore-on-opaque-inference=yes 177 | 178 | # List of class names for which member attributes should not be checked (useful 179 | # for classes with dynamically set attributes). This supports the use of 180 | # qualified names. 181 | ignored-classes=optparse.Values,thread._local,_thread._local 182 | 183 | # List of module names for which member attributes should not be checked 184 | # (useful for modules/projects where namespaces are manipulated during runtime 185 | # and thus existing member attributes cannot be deduced by static analysis. It 186 | # supports qualified module names, as well as Unix pattern matching. 187 | ignored-modules= 188 | 189 | # Show a hint with possible names when a member name was not found. The aspect 190 | # of finding the hint is based on edit distance. 191 | missing-member-hint=yes 192 | 193 | # The minimum edit distance a name should have in order to be considered a 194 | # similar match for a missing member name. 195 | missing-member-hint-distance=1 196 | 197 | # The total number of similar names that should be taken in consideration when 198 | # showing a hint for a missing member. 199 | missing-member-max-choices=1 200 | 201 | 202 | [VARIABLES] 203 | 204 | # List of additional names supposed to be defined in builtins. Remember that 205 | # you should avoid defining new builtins when possible. 206 | additional-builtins= 207 | 208 | # Tells whether unused global variables should be treated as a violation. 209 | allow-global-unused-variables=yes 210 | 211 | # List of strings which can identify a callback function by name. A callback 212 | # name must start or end with one of those strings. 213 | callbacks=cb_, 214 | _cb 215 | 216 | # A regular expression matching the name of dummy variables (i.e. expected to 217 | # not be used). 218 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 219 | 220 | # Argument names that match this expression will be ignored. Default to name 221 | # with leading underscore. 222 | ignored-argument-names=_.*|^ignored_|^unused_ 223 | 224 | # Tells whether we should check for unused import in __init__ files. 225 | init-import=no 226 | 227 | # List of qualified module names which can have objects that can redefine 228 | # builtins. 229 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 230 | 231 | 232 | [FORMAT] 233 | 234 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 235 | expected-line-ending-format= 236 | 237 | # Regexp for a line that is allowed to be longer than the limit. 238 | ignore-long-lines=^\s*(# )??$ 239 | 240 | # Number of spaces of indent required inside a hanging or continued line. 241 | indent-after-paren=4 242 | 243 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 244 | # tab). 245 | indent-string=' ' 246 | 247 | # Maximum number of characters on a single line. 248 | max-line-length=120 249 | 250 | # Maximum number of lines in a module. 251 | max-module-lines=1000 252 | 253 | # Allow the body of a class to be on the same line as the declaration if body 254 | # contains single statement. 255 | single-line-class-stmt=no 256 | 257 | # Allow the body of an if to be on the same line as the test if there is no 258 | # else. 259 | single-line-if-stmt=no 260 | 261 | 262 | [SIMILARITIES] 263 | 264 | # Ignore comments when computing similarities. 265 | ignore-comments=yes 266 | 267 | # Ignore docstrings when computing similarities. 268 | ignore-docstrings=yes 269 | 270 | # Ignore imports when computing similarities. 271 | ignore-imports=no 272 | 273 | # Minimum lines number of a similarity. 274 | min-similarity-lines=4 275 | 276 | 277 | [BASIC] 278 | 279 | # Naming style matching correct argument names. 280 | argument-naming-style=snake_case 281 | 282 | # Regular expression matching correct argument names. Overrides argument- 283 | # naming-style. 284 | #argument-rgx= 285 | 286 | # Naming style matching correct attribute names. 287 | attr-naming-style=snake_case 288 | 289 | # Regular expression matching correct attribute names. Overrides attr-naming- 290 | # style. 291 | #attr-rgx= 292 | 293 | # Bad variable names which should always be refused, separated by a comma. 294 | bad-names=foo, 295 | bar, 296 | baz, 297 | toto, 298 | tutu, 299 | tata 300 | 301 | # Naming style matching correct class attribute names. 302 | class-attribute-naming-style=any 303 | 304 | # Regular expression matching correct class attribute names. Overrides class- 305 | # attribute-naming-style. 306 | #class-attribute-rgx= 307 | 308 | # Naming style matching correct class names. 309 | class-naming-style=PascalCase 310 | 311 | # Regular expression matching correct class names. Overrides class-naming- 312 | # style. 313 | #class-rgx= 314 | 315 | # Naming style matching correct constant names. 316 | const-naming-style=UPPER_CASE 317 | 318 | # Regular expression matching correct constant names. Overrides const-naming- 319 | # style. 320 | #const-rgx= 321 | 322 | # Minimum line length for functions/classes that require docstrings, shorter 323 | # ones are exempt. 324 | docstring-min-length=-1 325 | 326 | # Naming style matching correct function names. 327 | function-naming-style=snake_case 328 | 329 | # Regular expression matching correct function names. Overrides function- 330 | # naming-style. 331 | #function-rgx= 332 | 333 | # Good variable names which should always be accepted, separated by a comma. 334 | good-names=i, 335 | j, 336 | k, 337 | ex, 338 | Run, 339 | _ 340 | 341 | # Include a hint for the correct naming format with invalid-name. 342 | include-naming-hint=no 343 | 344 | # Naming style matching correct inline iteration names. 345 | inlinevar-naming-style=any 346 | 347 | # Regular expression matching correct inline iteration names. Overrides 348 | # inlinevar-naming-style. 349 | #inlinevar-rgx= 350 | 351 | # Naming style matching correct method names. 352 | method-naming-style=snake_case 353 | 354 | # Regular expression matching correct method names. Overrides method-naming- 355 | # style. 356 | #method-rgx= 357 | 358 | # Naming style matching correct module names. 359 | module-naming-style=any 360 | 361 | # Regular expression matching correct module names. Overrides module-naming- 362 | # style. 363 | #module-rgx= 364 | 365 | # Colon-delimited sets of names that determine each other's naming style when 366 | # the name regexes allow several styles. 367 | name-group= 368 | 369 | # Regular expression which should only match function or class names that do 370 | # not require a docstring. 371 | no-docstring-rgx=^_ 372 | 373 | # List of decorators that produce properties, such as abc.abstractproperty. Add 374 | # to this list to register other decorators that produce valid properties. 375 | # These decorators are taken in consideration only for invalid-name. 376 | property-classes=abc.abstractproperty 377 | 378 | # Naming style matching correct variable names. 379 | variable-naming-style=snake_case 380 | 381 | # Regular expression matching correct variable names. Overrides variable- 382 | # naming-style. 383 | #variable-rgx= 384 | 385 | 386 | [STRING] 387 | 388 | # This flag controls whether the implicit-str-concat-in-sequence should 389 | # generate a warning on implicit string concatenation in sequences defined over 390 | # several lines. 391 | check-str-concat-over-line-jumps=no 392 | 393 | 394 | [IMPORTS] 395 | 396 | # Allow wildcard imports from modules that define __all__. 397 | allow-wildcard-with-all=no 398 | 399 | # Analyse import fallback blocks. This can be used to support both Python 2 and 400 | # 3 compatible code, which means that the block might have code that exists 401 | # only in one or another interpreter, leading to false positives when analysed. 402 | analyse-fallback-blocks=no 403 | 404 | # Deprecated modules which should not be used, separated by a comma. 405 | deprecated-modules=optparse,tkinter.tix 406 | 407 | # Create a graph of external dependencies in the given file (report RP0402 must 408 | # not be disabled). 409 | ext-import-graph= 410 | 411 | # Create a graph of every (i.e. internal and external) dependencies in the 412 | # given file (report RP0402 must not be disabled). 413 | import-graph= 414 | 415 | # Create a graph of internal dependencies in the given file (report RP0402 must 416 | # not be disabled). 417 | int-import-graph= 418 | 419 | # Force import order to recognize a module as part of the standard 420 | # compatibility libraries. 421 | known-standard-library= 422 | 423 | # Force import order to recognize a module as part of a third party library. 424 | known-third-party=enchant 425 | 426 | 427 | [CLASSES] 428 | 429 | # List of method names used to declare (i.e. assign) instance attributes. 430 | defining-attr-methods=__init__, 431 | __new__, 432 | setUp 433 | 434 | # List of member names, which should be excluded from the protected access 435 | # warning. 436 | exclude-protected=_asdict, 437 | _fields, 438 | _replace, 439 | _source, 440 | _make 441 | 442 | # List of valid names for the first argument in a class method. 443 | valid-classmethod-first-arg=cls 444 | 445 | # List of valid names for the first argument in a metaclass class method. 446 | valid-metaclass-classmethod-first-arg=cls 447 | 448 | 449 | [DESIGN] 450 | 451 | # Maximum number of arguments for function / method. 452 | max-args=5 453 | 454 | # Maximum number of attributes for a class (see R0902). 455 | max-attributes=7 456 | 457 | # Maximum number of boolean expressions in an if statement. 458 | max-bool-expr=5 459 | 460 | # Maximum number of branch for function / method body. 461 | max-branches=12 462 | 463 | # Maximum number of locals for function / method body. 464 | max-locals=15 465 | 466 | # Maximum number of parents for a class (see R0901). 467 | max-parents=7 468 | 469 | # Maximum number of public methods for a class (see R0904). 470 | max-public-methods=20 471 | 472 | # Maximum number of return / yield for function / method body. 473 | max-returns=6 474 | 475 | # Maximum number of statements in function / method body. 476 | max-statements=50 477 | 478 | # Minimum number of public methods for a class (see R0903). 479 | min-public-methods=2 480 | 481 | 482 | [EXCEPTIONS] 483 | 484 | # Exceptions that will emit a warning when being caught. Defaults to 485 | # "BaseException, Exception". 486 | overgeneral-exceptions=builtins.BaseException 487 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/alpine:latest@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c as builder 2 | 3 | COPY alertmanager-notifier/requirements.txt /work/alertmanager-notifier/requirements.txt 4 | 5 | ENV CRYPTOGRAPHY_DONT_BUILD_RUST="1" 6 | 7 | RUN set -xeu; \ 8 | mkdir -p /work/wheels; \ 9 | apk add \ 10 | python3-dev \ 11 | py3-pip \ 12 | openssl-dev \ 13 | gcc \ 14 | musl-dev \ 15 | libffi-dev \ 16 | make \ 17 | openssl-dev \ 18 | cargo \ 19 | ; \ 20 | pip3 install -U --break-system-packages \ 21 | wheel \ 22 | pip 23 | 24 | RUN pip3 wheel --prefer-binary -r /work/alertmanager-notifier/requirements.txt -w /work/wheels 25 | 26 | FROM public.ecr.aws/docker/library/alpine:latest@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c 27 | 28 | LABEL org.opencontainers.image.authors="alertmanager-notifier@docker.egos.tech" \ 29 | org.opencontainers.image.source="https://gitlab.com/ix.ai/alertmanager-notifie" \ 30 | org.opencontainers.image.url="egos.tech/alertmanager-notifier" 31 | 32 | COPY --from=builder /work / 33 | 34 | RUN set -xeu; \ 35 | ls -lashi /wheels; \ 36 | apk add --no-cache py3-pip; \ 37 | pip3 install \ 38 | -U \ 39 | --break-system-packages \ 40 | --no-index \ 41 | --no-cache-dir \ 42 | --find-links /wheels \ 43 | --requirement /alertmanager-notifier/requirements.txt \ 44 | ; \ 45 | rm -rf /wheels 46 | 47 | COPY alertmanager-notifier/ /alertmanager-notifier 48 | COPY templates/ /templates 49 | COPY alertmanager-notifier.sh /usr/local/bin/alertmanager-notifier.sh 50 | 51 | EXPOSE 9119 52 | 53 | ENTRYPOINT ["/usr/local/bin/alertmanager-notifier.sh"] 54 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alertmanager-notifier 2 | 3 | ## Deprecation Notice 4 | 5 | **This project is deprecated and has been archived**. Please switch to [gitlab.com/egos-tech/alertmanager-notifier](https://gitlab.com/egos-tech/alertmanager-notifier). 6 | 7 | Replace your docker image with `registry.gitlab.com/egos-tech/alertmanager-notifier:latest`. 8 | 9 | Please note, a new versioning format is established, starting with `1.0.0` - this version is one-to-one compatible with the latest version in this repository: 10 | 11 | ```yml 12 | image: registry.gitlab.com/egos-tech/alertmanager-notifier:1.0.0 13 | ``` 14 | 15 | All future updates will only be done to that project. 16 | 17 | ## Description 18 | 19 | [![Pipeline Status](https://gitlab.com/ix.ai/alertmanager-notifier/badges/master/pipeline.svg)](https://gitlab.com/ix.ai/alertmanager-notifier/) 20 | [![Gitlab Project](https://img.shields.io/badge/GitLab-Project-554488.svg)](https://gitlab.com/ix.ai/alertmanager-notifier/) 21 | 22 | A notifier for [alertmanager](https://github.com/prometheus/alertmanager), written in python. It supports multiple notification channels and new ones can be easily added. 23 | 24 | ## Deprecation 25 | 26 | **Telegram Deprecation**: Starting with the version 0.5.0 of `alertmanager-notifier`, Telegram support is dropped. As such, the documentation has been updated to reflect it. 27 | 28 | ## Running a simple test 29 | 30 | ```sh 31 | docker run --rm -it \ 32 | -p 8899:8899 \ 33 | -e GOTIFY_URL="https://gotify" \ 34 | -e GOTIFY_TOKEN="your gotify token" \ 35 | -e EXCLUDE_LABELS="yes" \ 36 | --name alertmanager-notifier \ 37 | registry.gitlab.com/ix.ai/alertmanager-notifier:latest 38 | ``` 39 | 40 | Run the test agains the bot: 41 | 42 | ```sh 43 | 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 44 | ``` 45 | 46 | ## Configure alertmanager 47 | 48 | ```yml 49 | route: 50 | receiver: 'alertmanager webhook' 51 | routes: 52 | - receiver: 'alertmanager-notifier-webhook' 53 | 54 | receivers: 55 | - name: 'alertmanager-notifier-webhook' 56 | webhook_configs: 57 | - url: http://alertmanager-notifier:8899/alert 58 | ``` 59 | 60 | ## Supported environment variables 61 | 62 | | **Variable** | **Default** | **Description** | 63 | |:--------------------|:----------------:|:---------------------------------------------------------------------------------------------------------------------------| 64 | | `GOTIFY_URL` | - | the URL of the [Gotify](https://gotify.net/) server | 65 | | `GOTIFY_TOKEN` | - | the APP token for Gotify | 66 | | `GOTIFY_TEMPLATE` | `markdown.md.j2` | allows you to specify another (HTML) template, in case you've mounted it under `/templates` | 67 | | `EXCLUDE_LABELS` | `yes` | set this to `no` to include the labels from the notifications | 68 | | `LOGLEVEL` | `INFO` | [Logging Level](https://docs.python.org/3/library/logging.html#levels) | 69 | | `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 | 70 | | `GELF_PORT` | `12201` | Ignored, if `GELF_HOST` is unset. The UDP port for GELF logging | 71 | | `PORT` | `8899` | the port for incoming connections | 72 | | `ADDRESS` | `*` | the address for the bot to listen on | 73 | 74 | **NOTE**: If no notifier is configured, the `Null` notifier will be used and the notification will only be logged 75 | 76 | ## Gotify Priority 77 | 78 | Gotify supports message priorities, that are also mapped to Android Importance (see [gotify/android#18](https://github.com/gotify/android/issues/18)). 79 | 80 | If you set the *annotation* `priority` to your alert, with a number as value, this will be passed through to gotify. 81 | 82 | **Note**: Since alertmanager supports sending multiple alerts in one message, alertmanager-notifier will always use the **highest** priority value for gotify from the batch. 83 | 84 | ## Templating 85 | 86 | **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`). 87 | 88 | ## Tags and Arch 89 | 90 | The images are multi-arch, with builds for amd64, arm64, armv7 and armv6. 91 | 92 | * `vN.N.N` - for example v0.0.1 93 | * `latest` - always pointing to the latest version 94 | * `dev-master` - the last build on the master branch 95 | 96 | ### Images 97 | 98 | * Gitlab Registry: `registry.gitlab.com/ix.ai/alertmanager-notifier` - [gitlab.com/ix.ai/alertmanager-notifier](https://gitlab.com/ix.ai/alertmanager-notifier) 99 | * GitHub Registry: `ghcr.io/ix-ai/alertmanager-notifier` [github.com/ix-ai/alertmanager-notifier](https://github.com/ix-ai/alertmanager-notifier) 100 | * Docker Hub: `ixdotai/alertmanager-notifier` - [hub.docker.com/r/ixdotai/alertmanager-notifier](https://hub.docker.com/r/ixdotai/alertmanager-notifier) 101 | -------------------------------------------------------------------------------- /alertmanager-notifier.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | exec python3 -m "alertmanager-notifier.alertmanager-notifier" "$@" 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /alertmanager-notifier/alertmanager-notifier.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ Web server that translates alertmanager alerts into messages for other services """ 3 | 4 | import logging 5 | import os 6 | from waitress import serve 7 | from flask import Flask 8 | from flask import request 9 | from .lib import constants 10 | from .lib import notifiers 11 | from .lib.utils import redact 12 | from .lib.utils import convert_type 13 | 14 | a = Flask(__name__, template_folder='../templates') 15 | a.secret_key = os.urandom(64).hex() 16 | 17 | log = logging.getLogger(__package__) 18 | version = f'{constants.VERSION}-{constants.BUILD}' 19 | 20 | 21 | @a.route('/alert', methods=['POST']) 22 | def parse_request(): 23 | """ Receives the alert and sends the notification """ 24 | content = request.get_json() 25 | 26 | return_message = "" 27 | try: 28 | log.info(f"Received {len(content['alerts'])} alert(s).") 29 | log.debug(f'Parsing content: {content}') 30 | # pylint: disable-next=possibly-used-before-assignment 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 | 'gotify_url': { 53 | 'type': 'string', 54 | }, 55 | 'gotify_token': { 56 | 'type': 'string', 57 | 'redact': True, 58 | }, 59 | 'port': { 60 | 'type': 'integer', 61 | 'default': '8899', 62 | }, 63 | 'address': { 64 | 'type': 'string', 65 | 'default': '*', 66 | }, 67 | 'gotify_template': { 68 | 'type': 'string', 69 | 'default': 'markdown.md.j2', 70 | }, 71 | 'null_template': { 72 | 'type': 'string', 73 | 'default': 'text.j2', 74 | }, 75 | 'exclude_labels': { 76 | 'type': 'boolean', 77 | 'default': 'yes', 78 | } 79 | } 80 | 81 | settings = { 82 | 'notifiers': [], 83 | } 84 | 85 | for param, param_settings in params.items(): 86 | try: 87 | settings.update({param: convert_type(os.environ[param.upper()], param_settings['type'])}) 88 | except ValueError: # Wrong value for the environment variable 89 | log.warning(f"`{os.environ[param.upper()]}` not understood for {param.upper()}. Ignoring.") 90 | except KeyError: # No environment variable set for the param 91 | pass 92 | 93 | try: 94 | if param not in settings: 95 | settings[param] = convert_type(param_settings['default'], param_settings['type']) 96 | log.info(f'{param.upper()} is set to `{redact(params, settings, settings[param])}`') 97 | except KeyError: # No default value for the param 98 | pass 99 | 100 | try: 101 | if settings['gotify_url'] and settings['gotify_token']: 102 | settings['notifiers'].append('gotify') 103 | except KeyError: 104 | pass 105 | 106 | log.info(f"Starting {__package__} {version}, listening on {settings['address']}:{settings['port']}") 107 | return settings 108 | 109 | 110 | if __name__ == '__main__': 111 | options = startup() 112 | try: 113 | if not options['notifiers']: 114 | log.warning('No notifier configured. Using `null`') 115 | n = notifiers.start(**options) 116 | except (ValueError) as error: 117 | log.error(error) 118 | else: 119 | serve(a, host=options['address'], port=options['port'], ident=f'{__package__} {version}') 120 | -------------------------------------------------------------------------------- /alertmanager-notifier/lib/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 = getattr(self, 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 null_notify(self, notifier, **kwargs): 63 | """ dispatches directly """ 64 | # pylint: disable=no-member 65 | log.debug('Sending message to null') 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.null_template, 72 | exclude_labels=self.exclude_labels, 73 | ) 74 | return notifier.send(**processed_alerts) 75 | -------------------------------------------------------------------------------- /alertmanager-notifier/lib/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ Various utilities """ 4 | 5 | import logging 6 | from flask import render_template 7 | 8 | log = logging.getLogger(__package__) 9 | 10 | 11 | def strtobool(val): 12 | """Convert a string representation of truth to true (1) or false (0). 13 | True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values 14 | are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if 15 | 'val' is anything else. 16 | """ 17 | val = val.lower() 18 | if val in ('y', 'yes', 't', 'true', 'on', '1'): 19 | return 1 20 | if val in ('n', 'no', 'f', 'false', 'off', '0'): 21 | return 0 22 | raise ValueError(f"invalid truth value {(val,)}") 23 | 24 | def redact(params: dict, settings: dict, message: str) -> str: 25 | """ 26 | based on params and the values of settings, it replaces sensitive 27 | information in message with a redacted string 28 | """ 29 | for param, setting in params.items(): 30 | if setting.get('redact') and settings.get(param): 31 | message = str(message).replace(settings.get(param), 'xxxREDACTEDxxx') 32 | return message 33 | 34 | 35 | def convert_type(param: str, target: str): 36 | """ 37 | converts string param to type target 38 | """ 39 | converted = param 40 | if target == "boolean": 41 | converted = bool(strtobool(param)) 42 | if target == "integer": 43 | converted = int(param) 44 | return converted 45 | 46 | 47 | def template_message(include_title=False, template='markdown.md.j2', exclude_labels=True, current_length=0, **kwargs): 48 | """ 49 | Formats the alerts for markdown notifiers 50 | 51 | Use `include_title` to specify if the title should be included in the message. 52 | If it's set to `False`, a separate key `title` will be returned. 53 | 54 | @return: False if the message processing fails otherwise dict 55 | """ 56 | processed = {'message': ''} 57 | alerts_count = len(kwargs['alerts']) 58 | title = f"{alerts_count} alert(s) received" 59 | if not include_title: 60 | processed.update({'title': f"{title}"}) 61 | title = None 62 | processed['message'] = render_template( 63 | template, 64 | title=title, 65 | alerts=kwargs['alerts'], 66 | external_url=kwargs['external_url'], 67 | receiver=kwargs['receiver'], 68 | exclude_labels=exclude_labels, 69 | current_length=current_length, 70 | ) 71 | for alert in kwargs['alerts']: 72 | if int(alert['annotations'].get('priority', -1)) > processed.get('priority', -1): 73 | processed['priority'] = int(alert['annotations']['priority']) 74 | return processed 75 | -------------------------------------------------------------------------------- /alertmanager-notifier/requirements.txt: -------------------------------------------------------------------------------- 1 | pygelf==0.4.2 2 | ix_notifiers==0.1.1754484830 3 | waitress==3.0.2 4 | flask 5 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo "Setting VERSION" 4 | find . -name .git -type d -prune -o -type f -name constants.py -exec sed -i "s/^VERSION.*/VERSION\ =\ \'${CI_COMMIT_REF_NAME:-None}\'/g" {} + -exec grep VERSION {} + 5 | echo "Setting BUILD" 6 | find . -name .git -type d -prune -o -type f -name constants.py -exec sed -i "s/^BUILD.*/BUILD\ =\ \'${CI_PIPELINE_ID:-None}\'/g" {} + -exec grep BUILD {} + 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>egos-tech/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------