├── superhooks ├── __init__.py ├── tests │ ├── __init__.py │ └── superhooks_test.py └── superhooks.py ├── CONTRIBUTORS.txt ├── CHANGES.md ├── requirements.txt ├── COPYRIGHT.txt ├── Release.md ├── .gitignore ├── LICENSE.txt ├── README.md └── setup.py /superhooks/__init__.py: -------------------------------------------------------------------------------- 1 | # superhooks package 2 | -------------------------------------------------------------------------------- /superhooks/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # superhooks package 2 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Contributors 2 | ------------ 3 | 4 | - Yuvaraj Loganathan, 2019-05-10 -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 0.5 (2019-06-2) 2 | ---------------- 3 | - Sending the complete supervisor data under the key name `pheaders_all` 4 | 5 | 0.4 (2019-05-11) 6 | ---------------- 7 | - Switched from semicolon(;) to cap(^) as separator 8 | 9 | 0.2 (2019-05-11) 10 | ---------------- 11 | - Fixed Readme 12 | 13 | 0.1 (2019-05-11) 14 | ---------------- 15 | - Initial release 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bleach==3.1.4 2 | certifi==2019.3.9 3 | chardet==3.0.4 4 | docutils==0.14 5 | idna==2.8 6 | meld3==1.0.2 7 | mock==3.0.5 8 | pkginfo==1.5.0.1 9 | Pygments==2.4.0 10 | readme-renderer==24.0 11 | requests==2.21.0 12 | requests-toolbelt==0.9.1 13 | six==1.12.0 14 | superlance==1.0.0 15 | supervisor==4.0.2 16 | tqdm==4.31.1 17 | twine==1.13.0 18 | urllib3==1.24.3 19 | webencodings==0.5.1 20 | -------------------------------------------------------------------------------- /COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Superlance is Copyright (c) 2019 Yuvaraj Loganathan 2 | All Rights Reserved 3 | 4 | Superhooks is Copyright (c) 2019 Yuvaraj Loganathan 5 | 6 | This software is subject to the provisions of the license at 7 | http://www.repoze.org/LICENSE.txt . A copy of this license should 8 | accompany this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND 9 | ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, 10 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, 11 | MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS FOR A PARTICULAR 12 | PURPOSE. 13 | 14 | -------------------------------------------------------------------------------- /Release.md: -------------------------------------------------------------------------------- 1 | ### Release Steps 2 | Create 2 virtualenv to generate package for both python 2 & 3 3 | 4 | 5 | ```bash 6 | virtualenv -p /usr/bin/python3 venv 7 | virtualenv -p /usr/bin/python2 venv2 8 | # activate python3 venv 9 | source venv/bin/activate 10 | pip install -r requirements.txt 11 | python3 setup.py sdist bdist_wheel 12 | deactivate 13 | # switch to python2 venv2 14 | source venv2/bin/activate 15 | pip install -r requirements.txt 16 | python2.7 setup.py sdist bdist_wheel 17 | # Check for Markdown error 18 | twine check dist/* 19 | # Replace the 0.5 with current version 20 | twine upload dist/superhooks-0.5.tar.gz 21 | twine upload dist/superhooks-0.5-py2-none-any.whl 22 | twine upload dist/superhooks-0.5-py3-none-any.whl 23 | ``` 24 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #VSCode 62 | .vscode/ 63 | .vs/ 64 | .idea/ 65 | venv/ 66 | venv2/ 67 | -------------------------------------------------------------------------------- /superhooks/tests/superhooks_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import mock 4 | 5 | 6 | class SuperHooksTests(unittest.TestCase): 7 | url = 'http://localhost:8090/' 8 | unexpected_err_msg = 'bar:foo;BACKOFF;PROCESS_STATE_FATAL;processname:foo groupname:bar from_state:BACKOFF ' 9 | events = 'FATAL,EXITED' 10 | 11 | def _get_target_class(self): 12 | from superhooks.superhooks import SuperHooks 13 | return SuperHooks 14 | 15 | def _make_one_mocked(self, **kwargs): 16 | kwargs['url'] = kwargs.get('url', self.url) 17 | kwargs['events'] = kwargs.get('events', self.events) 18 | 19 | obj = self._get_target_class()(**kwargs) 20 | obj.send_message = mock.Mock() 21 | return obj 22 | 23 | def get_process_fatal_event(self, pname, gname): 24 | headers = { 25 | 'ver': '3.0', 'poolserial': '7', 'len': '71', 26 | 'server': 'supervisor', 'eventname': 'PROCESS_STATE_FATAL', 27 | 'serial': '7', 'pool': 'superhooks', 28 | } 29 | payload = 'processname:{} groupname:{} from_state:BACKOFF'.format(pname, gname) 30 | return (headers, payload) 31 | 32 | def test_get_process_state_change_msg(self): 33 | crash = self._make_one_mocked() 34 | hdrs, payload = self.get_process_fatal_event('foo', 'bar') 35 | msg = crash.get_process_state_change_msg(hdrs, payload) 36 | self.assertEqual(self.unexpected_err_msg, msg) 37 | 38 | 39 | if __name__ == '__main__': 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Superhooks is licensed under the following license: 2 | 3 | A copyright notice accompanies this license document that identifies 4 | the copyright holders. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | 1. Redistributions in source code must retain the accompanying 11 | copyright notice, this list of conditions, and the following 12 | disclaimer. 13 | 14 | 2. Redistributions in binary form must reproduce the accompanying 15 | copyright notice, this list of conditions, and the following 16 | disclaimer in the documentation and/or other materials provided 17 | with the distribution. 18 | 19 | 3. Names of the copyright holders must not be used to endorse or 20 | promote products derived from this software without prior 21 | written permission from the copyright holders. 22 | 23 | 4. If any files are modified, you must cause the modified files to 24 | carry prominent notices stating that you changed the files and 25 | the date of any change. 26 | 27 | Disclaimer 28 | 29 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND 30 | ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 31 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 32 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 33 | HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 34 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 35 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 36 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 37 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 38 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 39 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 40 | SUCH DAMAGE. 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Superhooks 2 | 3 | Superhooks is a supervisor "event listener" that sends events from processes that run under [supervisor](http://supervisord.org) to predefined web hooks. When `superhooks` receives an event, it sends a message notification to a configured URL. 4 | 5 | `superhooks` uses [requests](https://2.python-requests.org/en/master/#) full-featured Python http requests library. 6 | 7 | ## Installation 8 | 9 | ``` 10 | pip install superhooks 11 | ``` 12 | 13 | ## Command-Line Syntax 14 | 15 | ```bash 16 | $ superhooks -u http://localhost:8090/ -e STARTING,RUNNING,BACKOFF,STOPPING,FATAL,EXITED,STOPPED,UNKNOWN -d "a^b^^c^d" -H "p^q^^r^s" 17 | ``` 18 | 19 | ### Options 20 | 21 | ```-u URL, --url=http://localhost:8090/``` 22 | 23 | Post the payload to the url with http `POST` 24 | 25 | ```-d DATA, --data=a^b^^c^d``` post body data as key value pair items are separated by `^^` and key and values are separated by `^` 26 | 27 | ```-H HEADERS, --headers=p^q^^r^s``` request headers with as key value pair items are separated by `^^` and key and values are separated by `^` 28 | 29 | ```-e EVENTS, --event=EVENTS``` 30 | 31 | The Supervisor Process State event(s) to listen for. It can be any, one of, or all of STARTING, RUNNING, BACKOFF, STOPPING, EXITED, STOPPED, UNKNOWN. 32 | 33 | ## Configuration 34 | An `[eventlistener:x]` section must be placed in `supervisord.conf` in order for `superhooks` to do its work. See the “Events” chapter in the Supervisor manual for more information about event listeners. 35 | 36 | The following example assume that `superhooks` is on your system `PATH`. 37 | 38 | ``` 39 | [eventlistener:superhooks] 40 | command=python /usr/local/bin/superhooks -u http://localhost:8090/ -e BACKOFF,FATAL,EXITED,UNKNOWN -d "a^b^^c^d" -H "p^q^^r^s" 41 | events=PROCESS_STATE,TICK_60 42 | 43 | ``` 44 | ### The above configuration will produce following payload for an crashing process named envoy 45 | 46 | ``` 47 | POST / HTTP/1.1 48 | Host: localhost:8090 49 | Accept: */* 50 | Accept-Encoding: gzip, deflate 51 | Connection: keep-alive 52 | Content-Length: 177 53 | Content-Type: application/x-www-form-urlencoded 54 | P: q 55 | R: s 56 | User-Agent: python-requests/2.12.1 57 | 58 | from_state=RUNNING&a=b&c=d&event_name=PROCESS_STATE_EXITED&process_name=cat%3Ameow&pheaders_all=from_state%3ARUNNING+processname%3Ameow+pid%3A25232+expected%3A0+groupname%3Acat+ 59 | ``` 60 | 61 | ### Notes 62 | * All the events will be buffered for 1 min and pushed to web hooks. 63 | 64 | ### Development 65 | * Modify the changes. 66 | * Execute `python setup.py publish` 67 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # This software is subject to the provisions of the BSD-like license at 3 | # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany 4 | # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL 5 | # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, 6 | # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND 7 | # FITNESS FOR A PARTICULAR PURPOSE 8 | # 9 | ############################################################################## 10 | 11 | import os 12 | import sys 13 | 14 | py_version = sys.version_info[:2] 15 | 16 | if py_version < (2, 6): 17 | raise RuntimeError('On Python 2, superhooks requires Python 2.6 or later') 18 | elif (3, 0) < py_version < (3, 2): 19 | raise RuntimeError('On Python 3, superhooks requires Python 3.2 or later') 20 | 21 | from setuptools import setup, find_packages 22 | 23 | here = os.path.abspath(os.path.dirname(__file__)) 24 | try: 25 | README = open(os.path.join(here, 'README.md')).read() 26 | except (IOError, OSError): 27 | README = '' 28 | try: 29 | CHANGES = open(os.path.join(here, 'CHANGES.md')).read() 30 | except (IOError, OSError): 31 | CHANGES = '' 32 | # 'setup.py publish' shortcut. 33 | if sys.argv[-1] == 'publish': 34 | os.system('python2 setup.py sdist bdist_wheel') 35 | os.system('python3 setup.py sdist bdist_wheel') 36 | os.system('twine upload dist/superhooks*.tar.gz') 37 | os.system('twine upload dist/superhooks*.whl') 38 | sys.exit() 39 | 40 | setup(name='superhooks', 41 | version='0.5', 42 | license='BSD-derived (http://www.repoze.org/LICENSE.txt)', 43 | description='superhooks plugin for supervisord', 44 | long_description=README + '\n\n' + CHANGES, 45 | long_description_content_type='text/markdown', 46 | classifiers=[ 47 | "Development Status :: 3 - Alpha", 48 | 'Environment :: No Input/Output (Daemon)', 49 | 'Intended Audience :: System Administrators', 50 | 'Natural Language :: English', 51 | 'Operating System :: POSIX', 52 | 'Programming Language :: Python :: 2', 53 | 'Programming Language :: Python :: 2.6', 54 | 'Programming Language :: Python :: 2.7', 55 | 'Programming Language :: Python :: 3', 56 | 'Programming Language :: Python :: 3.2', 57 | 'Programming Language :: Python :: 3.3', 58 | 'Programming Language :: Python :: 3.4', 59 | 'Programming Language :: Python :: 3.5', 60 | 'Programming Language :: Python :: 3.6', 61 | 'Programming Language :: Python :: 3.7', 62 | 'Topic :: System :: Boot', 63 | 'Topic :: System :: Monitoring', 64 | 'Topic :: System :: Systems Administration', 65 | ], 66 | author='Yuvaraj Loganathan', 67 | author_email='uvaraj6@gmail.com', 68 | url="https://github.com/skyrocknroll/superhooks", 69 | maintainer="Yuvaraj Loganathan", 70 | maintainer_email="uvaraj6@gmail.com", 71 | keywords='supervisor web hooks monitoring', 72 | packages=find_packages(), 73 | include_package_data=True, 74 | zip_safe=False, 75 | install_requires=[ 76 | 'superlance', 77 | 'supervisor', 78 | 'requests', 79 | ], 80 | tests_require=[ 81 | 'supervisor', 82 | 'superlance', 83 | 'mock', 84 | 85 | ], 86 | test_suite='superhooks.tests', 87 | entry_points="""\ 88 | [console_scripts] 89 | superhooks = superhooks.superhooks:main 90 | """ 91 | ) 92 | -------------------------------------------------------------------------------- /superhooks/superhooks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ############################################################################## 4 | # This software is subject to the provisions of the BSD-like license at 5 | # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany 6 | # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL 7 | # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, 8 | # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND 9 | # FITNESS FOR A PARTICULAR PURPOSE 10 | # 11 | ############################################################################## 12 | 13 | # A event listener meant to be subscribed to PROCESS_STATE_CHANGE 14 | # events. It will send web hook messages when processes that are children of 15 | # supervisord transition unexpectedly to the EXITED state. 16 | 17 | # A supervisor config snippet that tells supervisor to use this script 18 | # as a listener is below. 19 | # 20 | # [eventlistener:superhooks] 21 | # command=python superhooks -u http://localhost:8090/ -e BACKOFF,STOPPING,FATAL,EXITED,STOPPED,UNKNOWN -i 1 -d a:b::c:d -H p:q::r:s 22 | # events=PROCESS_STATE,TICK_60 23 | 24 | """ 25 | Usage: superhooks [-u url] [-e events] 26 | 27 | Options: 28 | -h, --help show this help message and exit 29 | -u URL, --url=URL 30 | Web hook URL 31 | 32 | -e EVENTS, --events=EVENTS 33 | Supervisor process state event(s) 34 | """ 35 | 36 | import copy 37 | import os 38 | import sys 39 | 40 | import requests 41 | from superlance.process_state_monitor import ProcessStateMonitor 42 | from supervisor import childutils 43 | 44 | 45 | class SuperHooks(ProcessStateMonitor): 46 | SUPERVISOR_EVENTS = ( 47 | 'STARTING', 'RUNNING', 'BACKOFF', 'STOPPING', 48 | 'FATAL', 'EXITED', 'STOPPED', 'UNKNOWN', 49 | ) 50 | 51 | @classmethod 52 | def _get_opt_parser(cls): 53 | from optparse import OptionParser 54 | 55 | parser = OptionParser() 56 | parser.add_option("-u", "--url", help="Web Hook URL") 57 | parser.add_option("-d", "--data", help="data in key value pair ex: `foo:bar::goo:baz`") 58 | parser.add_option("-H", "--headers", help="headers in key value pair ex: `foo:bar::goo:baz`") 59 | parser.add_option("-e", "--events", 60 | help="Supervisor event(s). Can be any, some or all of {} as comma separated values".format( 61 | cls.SUPERVISOR_EVENTS)) 62 | 63 | return parser 64 | 65 | @classmethod 66 | def parse_cmd_line_options(cls): 67 | parser = cls._get_opt_parser() 68 | (options, args) = parser.parse_args() 69 | return options 70 | 71 | @classmethod 72 | def validate_cmd_line_options(cls, options): 73 | parser = cls._get_opt_parser() 74 | if not options.url: 75 | parser.print_help() 76 | sys.exit(1) 77 | if not options.events: 78 | parser.print_help() 79 | sys.exit(1) 80 | 81 | validated = copy.copy(options) 82 | return validated 83 | 84 | @classmethod 85 | def get_cmd_line_options(cls): 86 | return cls.validate_cmd_line_options(cls.parse_cmd_line_options()) 87 | 88 | @classmethod 89 | def create_from_cmd_line(cls): 90 | options = cls.get_cmd_line_options() 91 | 92 | if 'SUPERVISOR_SERVER_URL' not in os.environ: 93 | sys.stderr.write('Must run as a supervisor event listener\n') 94 | sys.exit(1) 95 | 96 | return cls(**options.__dict__) 97 | 98 | def __init__(self, **kwargs): 99 | ProcessStateMonitor.__init__(self, **kwargs) 100 | self.url = kwargs['url'] 101 | self.data = kwargs.get('data', None) 102 | self.headers = kwargs.get('headers', None) 103 | events = kwargs.get('events', None) 104 | self.process_state_events = [ 105 | 'PROCESS_STATE_{}'.format(e.strip().upper()) 106 | for e in events.split(",") 107 | if e in self.SUPERVISOR_EVENTS 108 | ] 109 | 110 | def get_process_state_change_msg(self, headers, payload): 111 | pheaders, pdata = childutils.eventdata(payload + '\n') 112 | pheaders_all = "" 113 | for k, v in pheaders.items(): 114 | pheaders_all = pheaders_all + k + ":" + v + " " 115 | return "{groupname}:{processname};{from_state};{event};{pheaders_all}".format( 116 | event=headers['eventname'], pheaders_all=pheaders_all, **pheaders 117 | ) 118 | 119 | def send_batch_notification(self): 120 | for msg in self.batchmsgs: 121 | processname, from_state, eventname, pheaders_all = msg.rsplit(';') 122 | params = {'process_name': processname, 'from_state': from_state, 'event_name': eventname, 123 | 'pheaders_all': pheaders_all} 124 | if self.data: 125 | for item in self.data.split("^^"): 126 | kv = item.split("^") 127 | if len(kv) == 2: 128 | params[kv[0]] = kv[1] 129 | headers = {} 130 | if self.headers: 131 | for item in self.headers.split("^^"): 132 | kv = item.split("^") 133 | if len(kv) == 2: 134 | headers[kv[0]] = kv[1] 135 | requests.post(self.url, data=params, headers=headers) 136 | 137 | 138 | def main(): 139 | superhooks = SuperHooks.create_from_cmd_line() 140 | superhooks.run() 141 | 142 | 143 | if __name__ == '__main__': 144 | main() 145 | --------------------------------------------------------------------------------