├── .githooks └── pre-commit ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── install_supervisor ├── pylintrc ├── requirements.txt ├── setup.py ├── supervisor_logging └── __init__.py ├── test_requirements.txt └── tests ├── __init__.py ├── messages ├── supervisord.conf └── test_supervisor_logging.py /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # pylint:disable=invalid-name 3 | """ 4 | Git pre-commit hook for performing quality checks on python code using pep8 5 | and pylint 6 | """ 7 | 8 | from __future__ import print_function 9 | 10 | import atexit 11 | 12 | import os 13 | import os.path 14 | 15 | from optparse import OptionParser 16 | 17 | import re 18 | 19 | import shutil 20 | 21 | from subprocess import Popen, PIPE, check_call, CalledProcessError 22 | 23 | import sys 24 | 25 | import tempfile 26 | 27 | # 28 | # Threshold for code to pass the Pylint test. 10 is the highest score Pylint 29 | # will give to any peice of code. 30 | # 31 | _PYLINT_PASS_THRESHOLD = 10 32 | 33 | PEP8_CONF = 'conf/pep8.conf' 34 | 35 | PYLINT_CONF = 'conf/pylint.conf' 36 | 37 | JSHINT_CONF = 'conf/jshint.conf' 38 | 39 | 40 | def output(command): 41 | """ 42 | Read the command output regardless of the exit code 43 | """ 44 | command = command.split() 45 | sub = Popen(command, stdout=PIPE) 46 | sub.wait() 47 | return sub.stdout.read().decode() 48 | 49 | 50 | class Main(object): 51 | """ 52 | Check the Python code in the project. 53 | """ 54 | def __init__(self, force=False): 55 | """ 56 | Initialise the checker. Set @force to True to check all the files 57 | and not just the changed ones. 58 | """ 59 | self.force = force 60 | 61 | def pep8(self): 62 | """ 63 | Run pep8 on the project. 64 | Nothing short of perfect will do for this. 65 | """ 66 | 67 | import pep8 68 | 69 | pep_opts = {} 70 | if os.path.exists(PEP8_CONF): 71 | pep_opts['config_file'] = PEP8_CONF 72 | 73 | style = pep8.StyleGuide(**pep_opts) 74 | report = style.check_files((self.index,)) 75 | count = report.total_errors 76 | if count > 0: 77 | print("Project failed pep8 check: %d error(s)." % count) 78 | cmd = "pep8 " 79 | if 'config_file' in pep_opts: 80 | cmd += "--config=%s " % pep_opts['config_file'] 81 | cmd += "." 82 | print("Re-run with:\n%s\n" % cmd) 83 | return False 84 | 85 | return True 86 | 87 | def pylint(self): 88 | """ 89 | Run PyLint on the project. 90 | """ 91 | 92 | # Build a list of targets 93 | targets = [] 94 | modules = self.modules() 95 | for module in modules: 96 | targets.append(module) 97 | for file_ in self.changed_py_files(): 98 | if not any(file_.startswith(m) for m in modules): 99 | targets.append(file_) 100 | 101 | ok = True 102 | for target in targets: 103 | cmd = 'pylint' 104 | 105 | if os.path.exists(PYLINT_CONF): 106 | cmd += ' --rcfile=' + PYLINT_CONF 107 | 108 | cmd += ' %%s%s' % target 109 | 110 | result = output(cmd % self.index) 111 | 112 | # 113 | # Get the rating from the result 114 | # 115 | rating = pylint_rating(result) 116 | if rating is not None and rating < _PYLINT_PASS_THRESHOLD: 117 | ok = False 118 | print( 119 | "%s failed PyLint check (scored %s, min allowed is %s)" 120 | % (target, rating, _PYLINT_PASS_THRESHOLD) 121 | ) 122 | print("Re-run with:\n%s\n" % (cmd % '')) 123 | 124 | return ok 125 | 126 | def jshint(self): 127 | """ 128 | Run jshint on the project. 129 | Nothing short of perfect will do for this either. 130 | """ 131 | 132 | js_files = self.changed_js_files() 133 | 134 | if not js_files: 135 | return True 136 | 137 | jsargs = ('jshint',) 138 | 139 | if os.path.exists(JSHINT_CONF): 140 | jsargs += ('-c', JSHINT_CONF) 141 | 142 | try: 143 | check_call(jsargs + tuple(js_files)) 144 | except CalledProcessError: 145 | print("JSHint failed") 146 | print("Re-run with:\n%s\n" % (' '.join(jsargs),)) 147 | return False 148 | 149 | return True 150 | 151 | def copy_index(self): 152 | """ 153 | Create a copy of index in a temporary directory. 154 | """ 155 | # pylint:disable=attribute-defined-outside-init 156 | self.index = tempfile.mkdtemp() + '/' 157 | output('git checkout-index --prefix=%s -af' % self.index) 158 | 159 | # pylint:disable=no-self-use 160 | def changed_files(self): 161 | """ 162 | A list of files changed in the index. 163 | """ 164 | if self.force: 165 | cmd = 'git ls-tree -r --name-only HEAD' 166 | else: 167 | cmd = 'git diff --staged --diff-filter=ACMRTUXB --name-only HEAD' 168 | return output(cmd).split() 169 | 170 | def changed_py_files(self): 171 | """ 172 | A list of Python files changed in the index 173 | """ 174 | def is_py_file(filename): 175 | """ 176 | Determine whether a script is a Python file 177 | """ 178 | if not os.path.exists(filename): 179 | return False 180 | if filename.endswith('.py'): 181 | return True 182 | 183 | with open(filename, 'r') as file_: 184 | first_line = file_.readline().strip() 185 | return '#!' in first_line and 'python' in first_line 186 | 187 | try: 188 | return self._changed_py_files 189 | except AttributeError: 190 | # pylint:disable=attribute-defined-outside-init 191 | self._changed_py_files = \ 192 | [f for f in self.changed_files() if is_py_file(f)] 193 | return self._changed_py_files 194 | 195 | def changed_js_files(self): 196 | """ 197 | A list of Javascript files changed in the index 198 | """ 199 | 200 | try: 201 | with open('.jshintignore') as file_: 202 | ignore = tuple(f.strip() for f in file_.readlines()) 203 | except IOError: 204 | ignore = () 205 | 206 | def is_js_file(filename): 207 | """ 208 | Determine whether a script is a Javascript file 209 | """ 210 | if not os.path.exists(filename): 211 | return False 212 | elif filename.startswith(ignore): 213 | return False 214 | elif filename.endswith('.js'): 215 | return True 216 | else: 217 | return False 218 | 219 | try: 220 | return self._changed_js_files 221 | except AttributeError: 222 | # pylint:disable=attribute-defined-outside-init 223 | self._changed_js_files = \ 224 | [f for f in self.changed_files() if is_js_file(f)] 225 | return self._changed_js_files 226 | 227 | def modules(self): 228 | """ 229 | A list of Python modules in the checkout. 230 | """ 231 | def is_module(module): 232 | """ 233 | Determine whether a directory contains a Python module. 234 | """ 235 | return os.path.exists(os.path.join(module, '__init__.py')) 236 | return list(m for m in os.listdir(self.index) if is_module(m)) 237 | 238 | def cleanup(self): 239 | """ 240 | Delete temporary files. 241 | """ 242 | shutil.rmtree(self.index, ignore_errors=True) 243 | 244 | def main(self): 245 | """ 246 | Run all the necessary checks. 247 | """ 248 | if not self.changed_py_files() and \ 249 | not self.changed_js_files() and \ 250 | not self.force: 251 | print("No source files changed. Pre-commit tests skipped.") 252 | return True 253 | atexit.register(self.cleanup) 254 | self.copy_index() 255 | good = [func() for func in (self.pep8, self.pylint, self.jshint)] 256 | if all(good): 257 | print("Your code looks good. Continuing with commit.") 258 | return True 259 | else: 260 | print("Pre-commit tests failed.") 261 | return False 262 | 263 | 264 | def pylint_rating(result): 265 | """ 266 | Extract the rating rating from PyLint output. 267 | """ 268 | 269 | if result == '': 270 | return None 271 | rating = re.search(r'Your code has been rated at ([-\d\.]+)/10', result) 272 | return float(rating.group(1)) 273 | 274 | 275 | if __name__ == '__main__': 276 | parser = OptionParser() 277 | parser.add_option('-f', '--force', action='store_true', dest='force', 278 | help='force a check even if no Python files changed') 279 | (options, args) = parser.parse_args() 280 | hook = Main(**options.__dict__) 281 | success = hook.main() 282 | sys.exit(0 if success else 1) 283 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | /*.egg 4 | /.coverage 5 | /build 6 | /dist 7 | /supervisor_logging.egg-info 8 | /htmlcov 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.2" 5 | - "3.3" 6 | - "3.4" 7 | env: 8 | - SUPERVISOR_PYTHON_VERSION=2.7 9 | install: 10 | - pip install -r requirements.txt 11 | - pip install -r test_requirements.txt 12 | - ./install_supervisor 13 | script: 14 | - .githooks/pre-commit -f 15 | - python -m unittest discover 16 | sudo: false 17 | deploy: 18 | provider: pypi 19 | user: ixa 20 | password: 21 | secure: "i/CUjDVtSLg74kuLlKCAMAxOrj6WkxZpsXZG4Cz+30S5LXVqhgRfm3Ac0OY4LwMepksTNAqUUcV81p2Vf05y+1fp9q2d6g1C5gG+80pAaO7rDlRA6ce1tRjQmT4YrPDp9V5ngY5R1Y9ZmKuRKP6XYMJb5tzVfPE+JxyXneC9Ez8=" 22 | server: https://pypi.python.org/pypi 23 | on: 24 | all_branches: true 25 | python: 2.7 26 | condition: "\"$TRAVIS_TAG\" = \"v$(python setup.py --version)\"" 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include requirements.txt 3 | include test_requirements.txt 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Supervisor-logging 2 | ================== 3 | 4 | A [supervisor] plugin to stream events to an external Syslog instance (for 5 | example, Logstash). 6 | 7 | Installation 8 | ------------ 9 | 10 | Python 2.7 or Python 3.2+ is required. 11 | 12 | ``` 13 | pip install supervisor-logging 14 | ``` 15 | 16 | Note that supervisor itself does not yet work on Python 3, though it can be 17 | installed in a separate environment (because supervisor-logging is a separate 18 | process). 19 | 20 | Usage 21 | ----- 22 | 23 | The Syslog instance to send the events to is configured with the environment 24 | variables: 25 | 26 | * `SYSLOG_SERVER` 27 | * `SYSLOG_PORT` 28 | * `SYSLOG_PROTO` 29 | 30 | Add the plugin as an event listener in your `supervisord.conf` file: 31 | 32 | ``` 33 | [eventlistener:logging] 34 | command = supervisor_logging 35 | events = PROCESS_LOG 36 | ``` 37 | 38 | Enable the log events in your program: 39 | 40 | ``` 41 | [program:yourprogram] 42 | stdout_events_enabled = true 43 | stderr_events_enabled = true 44 | ``` 45 | 46 | [supervisor]: http://supervisord.org/ 47 | -------------------------------------------------------------------------------- /install_supervisor: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # Install supervisord into Python 2.7 environment, regardless of the one 3 | # currently active, and make it available in PATH. 4 | 5 | . ~/virtualenv/python2.7/bin/activate 6 | 7 | pip install supervisor 8 | 9 | if [ "$SUPERVISOR_PYTHON_VERSION" != "$TRAVIS_PYTHON_VERSION" ] 10 | then 11 | ln -s ~/virtualenv/python$SUPERVISOR_PYTHON_VERSION/bin/supervisorctl \ 12 | ln -s ~/virtualenv/python$SUPERVISOR_PYTHON_VERSION/bin/supervisord \ 13 | ~/virtualenv/python$TRAVIS_PYTHON_VERSION/bin 14 | fi 15 | 16 | deactivate 17 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore= 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins=pylint_mccabe 23 | 24 | 25 | [REPORTS] 26 | 27 | # Set the output format. Available formats are text, parseable, colorized, msvs 28 | # (visual studio) and html. You can also give a reporter class, eg 29 | # mypackage.mymodule.MyReporterClass. 30 | output-format=colorized 31 | 32 | # Put messages in a separate file for each module / package specified on the 33 | # command line instead of printing them on stdout. Reports (if any) will be 34 | # written in a file name "pylint_global.[txt|html]". 35 | files-output=no 36 | 37 | # Tells whether to display a full report or only the messages 38 | reports=yes 39 | 40 | # Python expression which should return a note less than 10 (10 is the highest 41 | # note). You have access to the variables errors warning, statement which 42 | # respectively contain the number of errors / warnings messages and the total 43 | # number of statements analyzed. This is used by the global evaluation report 44 | # (RP0004). 45 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 46 | 47 | # Add a comment according to your evaluation note. This is used by the global 48 | # evaluation report (RP0004). 49 | comment=no 50 | 51 | # Template used to display messages. This is a python new-style format string 52 | # used to format the message information. See doc for all details 53 | #msg-template= 54 | 55 | 56 | [MESSAGES CONTROL] 57 | 58 | # Enable the message, report, category or checker with the given id(s). You can 59 | # either give multiple identifier separated by comma (,) or put this option 60 | # multiple time. See also the "--disable" option for examples. 61 | #enable= 62 | 63 | # Disable the message, report, category or checker with the given id(s). You 64 | # can either give multiple identifiers separated by comma (,) or put this 65 | # option multiple times (only on the command line, not in the configuration 66 | # file where it should appear only once).You can also use "--disable=all" to 67 | # disable everything first and then reenable specific checks. For example, if 68 | # you want to run only the similarities checker, you can use "--disable=all 69 | # --enable=similarities". If you want to run only the classes checker, but have 70 | # no Warning level messages displayed, use"--disable=all --enable=classes 71 | # --disable=W" 72 | disable= 73 | abstract-method, 74 | fixme, 75 | locally-disabled, 76 | no-self-use, 77 | star-args, 78 | super-on-old-class, 79 | too-few-public-methods, 80 | too-many-public-methods, 81 | RP0101, 82 | RP0401, 83 | RP0402, 84 | RP0701, 85 | RP0801, 86 | 87 | 88 | [BASIC] 89 | 90 | # Required attributes for module, separated by a comma 91 | required-attributes= 92 | 93 | # List of builtins function names that should not be used, separated by a comma 94 | bad-functions=apply 95 | 96 | # Regular expression which should only match correct module names 97 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 98 | 99 | # Regular expression which should only match correct module level names 100 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 101 | 102 | # Regular expression which should only match correct class names 103 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 104 | 105 | # Regular expression which should only match correct function names 106 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 107 | 108 | # Regular expression which should only match correct method names 109 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 110 | 111 | # Regular expression which should only match correct instance attribute names 112 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 113 | 114 | # Regular expression which should only match correct argument names 115 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 116 | 117 | # Regular expression which should only match correct variable names 118 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 119 | 120 | # Regular expression which should only match correct attribute names in class 121 | # bodies 122 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 123 | 124 | # Regular expression which should only match correct list comprehension / 125 | # generator expression variable names 126 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 127 | 128 | # Good variable names which should always be accepted, separated by a comma 129 | good-names=i,j,k,ex,Run,_ 130 | 131 | # Bad variable names which should always be refused, separated by a comma 132 | bad-names=foo,bar,baz,toto,tutu,tata 133 | 134 | # Regular expression which should only match function or class names that do 135 | # not require a docstring. 136 | no-docstring-rgx=__.*__ 137 | 138 | # Minimum line length for functions/classes that require docstrings, shorter 139 | # ones are exempt. 140 | docstring-min-length=-1 141 | 142 | 143 | [TYPECHECK] 144 | 145 | # Tells whether missing members accessed in mixin class should be ignored. A 146 | # mixin class is detected if its name ends with "mixin" (case insensitive). 147 | ignore-mixin-members=yes 148 | 149 | # List of classes names for which member attributes should not be checked 150 | # (useful for classes with attributes dynamically set). 151 | ignored-classes=SQLObject 152 | 153 | # When zope mode is activated, add a predefined set of Zope acquired attributes 154 | # to generated-members. 155 | zope=no 156 | 157 | # List of members which are set dynamically and missed by pylint inference 158 | # system, and so shouldn't trigger E0201 when accessed. Python regular 159 | # expressions are accepted. 160 | generated-members=REQUEST,acl_users,aq_parent 161 | 162 | 163 | [FORMAT] 164 | 165 | # Maximum number of characters on a single line. 166 | max-line-length=80 167 | 168 | # Regexp for a line that is allowed to be longer than the limit. 169 | ignore-long-lines=^\s*(# )??$ 170 | 171 | # Allow the body of an if to be on the same line as the test if there is no 172 | # else. 173 | single-line-if-stmt=no 174 | 175 | # List of optional constructs for which whitespace checking is disabled 176 | no-space-check=trailing-comma,dict-separator 177 | 178 | # Maximum number of lines in a module 179 | max-module-lines=1000 180 | 181 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 182 | # tab). 183 | indent-string=' ' 184 | 185 | 186 | [SIMILARITIES] 187 | 188 | # Minimum lines number of a similarity. 189 | min-similarity-lines=4 190 | 191 | # Ignore comments when computing similarities. 192 | ignore-comments=yes 193 | 194 | # Ignore docstrings when computing similarities. 195 | ignore-docstrings=yes 196 | 197 | # Ignore imports when computing similarities. 198 | ignore-imports=yes 199 | 200 | 201 | [VARIABLES] 202 | 203 | # Tells whether we should check for unused import in __init__ files. 204 | init-import=no 205 | 206 | # A regular expression matching the beginning of the name of dummy variables 207 | # (i.e. not used). 208 | dummy-variables-rgx=_$|dummy 209 | 210 | # List of additional names supposed to be defined in builtins. Remember that 211 | # you should avoid to define new builtins when possible. 212 | additional-builtins= 213 | 214 | 215 | [MISCELLANEOUS] 216 | 217 | # List of note tags to take in consideration, separated by a comma. 218 | notes=FIXME,XXX,TODO 219 | 220 | 221 | [DESIGN] 222 | 223 | # Maximum number of arguments for function / method 224 | max-args=5 225 | 226 | # Argument names that match this expression will be ignored. Default to name 227 | # with leading underscore 228 | ignored-argument-names=_.* 229 | 230 | # Maximum number of locals for function / method body 231 | max-locals=15 232 | 233 | # Maximum number of return / yield for function / method body 234 | max-returns=6 235 | 236 | # Maximum number of branch for function / method body 237 | max-branches=12 238 | 239 | # Maximum number of statements in function / method body 240 | max-statements=50 241 | 242 | # Maximum number of parents for a class (see R0901). 243 | max-parents=7 244 | 245 | # Maximum number of attributes for a class (see R0902). 246 | max-attributes=7 247 | 248 | # Minimum number of public methods for a class (see R0903). 249 | min-public-methods=2 250 | 251 | # Maximum number of public methods for a class (see R0904). 252 | max-public-methods=20 253 | 254 | 255 | [CLASSES] 256 | 257 | # List of interface methods to ignore, separated by a comma. This is used for 258 | # instance to not check methods defines in Zope's Interface base class. 259 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 260 | 261 | # List of method names used to declare (i.e. assign) instance attributes. 262 | defining-attr-methods=__init__,__new__,setUp 263 | 264 | # List of valid names for the first argument in a class method. 265 | valid-classmethod-first-arg=cls 266 | 267 | # List of valid names for the first argument in a metaclass class method. 268 | valid-metaclass-classmethod-first-arg=mcs 269 | 270 | 271 | [IMPORTS] 272 | 273 | # Deprecated modules which should not be used, separated by a comma 274 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 275 | 276 | # Create a graph of every (i.e. internal and external) dependencies in the 277 | # given file (report RP0402 must not be disabled) 278 | import-graph= 279 | 280 | # Create a graph of external dependencies in the given file (report RP0402 must 281 | # not be disabled) 282 | ext-import-graph= 283 | 284 | # Create a graph of internal dependencies in the given file (report RP0402 must 285 | # not be disabled) 286 | int-import-graph= 287 | 288 | 289 | [EXCEPTIONS] 290 | 291 | # Exceptions that will emit a warning when being caught. Defaults to 292 | # "Exception" 293 | overgeneral-exceptions=Exception 294 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infoxchange/supervisor-logging/4b3abfa27d88357f6698868926ab7a9234669113/requirements.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2014 Infoxchange Australia 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | Setup script. 18 | """ 19 | 20 | from setuptools import setup, find_packages 21 | 22 | with open('requirements.txt') as requirements, \ 23 | open('test_requirements.txt') as test_requirements: 24 | setup( 25 | name='supervisor-logging', 26 | version='0.0.9', 27 | description='Stream supervisord logs to a syslog instance', 28 | author='Infoxchange development team', 29 | author_email='devs@infoxchange.net.au', 30 | url='https://github.com/infoxchange/supervisor-logging', 31 | license='Apache 2.0', 32 | long_description=open('README.md').read(), 33 | 34 | packages=find_packages(exclude=['tests']), 35 | package_data={ 36 | 'forklift': [ 37 | 'README.md', 38 | 'requirements.txt', 39 | 'test_requirements.txt', 40 | ], 41 | }, 42 | entry_points={ 43 | 'console_scripts': [ 44 | 'supervisor_logging = supervisor_logging:main', 45 | ], 46 | }, 47 | 48 | install_requires=requirements.read().splitlines(), 49 | 50 | test_suite='tests', 51 | tests_require=test_requirements.read().splitlines(), 52 | ) 53 | -------------------------------------------------------------------------------- /supervisor_logging/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2014 Infoxchange Australia 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | Send received events to a syslog instance. 19 | """ 20 | 21 | from __future__ import print_function 22 | 23 | import logging 24 | import os 25 | import re 26 | import socket 27 | import sys 28 | import time 29 | from logging.handlers import SysLogHandler 30 | 31 | 32 | class PalletFormatter(logging.Formatter): 33 | """ 34 | A formatter for the Pallet environment. 35 | """ 36 | 37 | HOSTNAME = re.sub( 38 | r':\d+$', '', os.environ.get('SITE_DOMAIN', socket.gethostname())) 39 | FORMAT = '%(asctime)s {hostname} %(name)s[%(process)d]: %(message)s'.\ 40 | format(hostname=HOSTNAME) 41 | DATE_FORMAT = '%Y-%m-%dT%H:%M:%S' 42 | 43 | converter = time.gmtime 44 | 45 | def __init__(self): 46 | super(PalletFormatter, self).__init__(fmt=self.FORMAT, 47 | datefmt=self.DATE_FORMAT) 48 | 49 | def formatTime(self, record, datefmt=None): 50 | """ 51 | Format time, including milliseconds. 52 | """ 53 | 54 | formatted = super(PalletFormatter, self).formatTime( 55 | record, datefmt=datefmt) 56 | return formatted + '.%03dZ' % record.msecs 57 | 58 | def format(self, record): 59 | # strip newlines 60 | message = super(PalletFormatter, self).format(record) 61 | message = message.replace('\n', ' ') 62 | message += '\n' 63 | return message 64 | 65 | 66 | def get_headers(line): 67 | """ 68 | Parse Supervisor message headers. 69 | """ 70 | 71 | return dict([x.split(':') for x in line.split()]) 72 | 73 | 74 | def eventdata(payload): 75 | """ 76 | Parse a Supervisor event. 77 | """ 78 | 79 | headerinfo, data = payload.split('\n', 1) 80 | headers = get_headers(headerinfo) 81 | return headers, data 82 | 83 | 84 | def supervisor_events(stdin, stdout): 85 | """ 86 | An event stream from Supervisor. 87 | """ 88 | 89 | while True: 90 | stdout.write('READY\n') 91 | stdout.flush() 92 | 93 | line = stdin.readline() 94 | headers = get_headers(line) 95 | 96 | payload = stdin.read(int(headers['len'])) 97 | event_headers, event_data = eventdata(payload) 98 | 99 | yield event_headers, event_data 100 | 101 | stdout.write('RESULT 2\nOK') 102 | stdout.flush() 103 | 104 | 105 | def main(): 106 | """ 107 | Main application loop. 108 | """ 109 | 110 | env = os.environ 111 | 112 | try: 113 | host = env['SYSLOG_SERVER'] 114 | port = int(env['SYSLOG_PORT']) 115 | socktype = socket.SOCK_DGRAM if env['SYSLOG_PROTO'] == 'udp' \ 116 | else socket.SOCK_STREAM 117 | except KeyError: 118 | sys.exit("SYSLOG_SERVER, SYSLOG_PORT and SYSLOG_PROTO are required.") 119 | 120 | handler = SysLogHandler( 121 | address=(host, port), 122 | socktype=socktype, 123 | ) 124 | handler.setFormatter(PalletFormatter()) 125 | 126 | for event_headers, event_data in supervisor_events(sys.stdin, sys.stdout): 127 | event = logging.LogRecord( 128 | name=event_headers['processname'], 129 | level=logging.INFO, 130 | pathname=None, 131 | lineno=0, 132 | msg=event_data, 133 | args=(), 134 | exc_info=None, 135 | ) 136 | event.process = int(event_headers['pid']) 137 | handler.handle(event) 138 | 139 | 140 | if __name__ == '__main__': 141 | main() 142 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | pep8 2 | pylint 3 | pylint-mccabe 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2014 Infoxchange Australia 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | Module placeholder for tests. 18 | """ 19 | -------------------------------------------------------------------------------- /tests/messages: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # Generate a test message. 3 | 4 | for i in {0..3} 5 | do 6 | echo Test $i 7 | sleep 2 8 | done 9 | -------------------------------------------------------------------------------- /tests/supervisord.conf: -------------------------------------------------------------------------------- 1 | ; Test supervisor configuration file 2 | 3 | [inet_http_server] 4 | port=0.0.0.0:9001 5 | 6 | [supervisord] 7 | logfile=/dev/null 8 | logfile_maxbytes=0 9 | logfile_backups=0 10 | pidfile=/tmp/supervisord.pid 11 | nodaemon=true 12 | 13 | ; the below section must remain in the config file for RPC 14 | ; (supervisorctl/web interface) to work, additional interfaces may be 15 | ; added by defining them in separate rpcinterface: sections 16 | [rpcinterface:supervisor] 17 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 18 | 19 | [eventlistener:logging] 20 | command = ./supervisor_logging/__init__.py 21 | events = PROCESS_LOG 22 | 23 | [program:messages] 24 | command=./tests/messages 25 | stdout_events_enabled=1 26 | -------------------------------------------------------------------------------- /tests/test_supervisor_logging.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2014 Infoxchange Australia 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | Test supervisor_logging. 18 | """ 19 | 20 | import os 21 | import re 22 | import socket 23 | try: 24 | import socketserver 25 | except ImportError: 26 | import SocketServer as socketserver 27 | import subprocess 28 | import threading 29 | 30 | from time import sleep 31 | 32 | from unittest import TestCase 33 | 34 | 35 | def strip_volatile(message): 36 | """ 37 | Strip volatile parts (PID, datetime) from a logging message. 38 | """ 39 | 40 | volatile = ( 41 | (socket.gethostname(), 'HOST'), 42 | (r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z', 'DATE'), 43 | ) 44 | 45 | for regexp, replacement in volatile: 46 | message = re.sub(regexp, replacement, message) 47 | 48 | return message 49 | 50 | 51 | class SupervisorLoggingTestCase(TestCase): 52 | """ 53 | Test logging. 54 | """ 55 | 56 | maxDiff = None 57 | 58 | def test_logging(self): 59 | """ 60 | Test logging. 61 | """ 62 | 63 | messages = [] 64 | 65 | class SyslogHandler(socketserver.BaseRequestHandler): 66 | """ 67 | Save received messages. 68 | """ 69 | 70 | def handle(self): 71 | messages.append(self.request[0].strip().decode()) 72 | 73 | syslog = socketserver.UDPServer(('0.0.0.0', 0), SyslogHandler) 74 | try: 75 | threading.Thread(target=syslog.serve_forever).start() 76 | 77 | env = os.environ.copy() 78 | env['SYSLOG_SERVER'] = syslog.server_address[0] 79 | env['SYSLOG_PORT'] = str(syslog.server_address[1]) 80 | env['SYSLOG_PROTO'] = 'udp' 81 | 82 | mydir = os.path.dirname(__file__) 83 | 84 | supervisor = subprocess.Popen( 85 | ['supervisord', '-c', os.path.join(mydir, 'supervisord.conf')], 86 | env=env, 87 | ) 88 | try: 89 | 90 | sleep(3) 91 | 92 | pid = subprocess.check_output( 93 | ['supervisorctl', 'pid', 'messages'] 94 | ).decode().strip() 95 | 96 | sleep(8) 97 | 98 | self.assertEqual( 99 | list(map(strip_volatile, messages)), 100 | ['<14>DATE HOST messages[{pid}]: Test {i} \n\x00'.format( 101 | pid=pid, 102 | i=i) 103 | for i in range(4)] 104 | ) 105 | finally: 106 | supervisor.terminate() 107 | 108 | finally: 109 | syslog.shutdown() 110 | --------------------------------------------------------------------------------