├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── healthcheck_single_test.py │ ├── environmentdump_test.py │ ├── security_test.py │ └── healthcheck_test.py └── web │ ├── __init__.py │ ├── flaskapp_test.py │ ├── flask_test.py │ └── tornado_test.py ├── healthcheck ├── py.typed ├── __init__.py ├── tornado_handler.py ├── timeout.py ├── security.py ├── environmentdump.py └── healthcheck.py ├── requirements-dev.txt ├── .bumpversion.cfg ├── py-healthcheck.jpg ├── .coveragerc ├── setup.cfg ├── .editorconfig ├── .gitignore ├── .pre-commit-config.yaml ├── .github └── workflows │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── tox.ini ├── LICENSE.txt ├── setup.py ├── CHANGELOG-legacy.md ├── Makefile ├── CONTRIBUTING.md ├── CHANGELOG.md ├── openapi └── openapi-healthcheck.yaml └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /healthcheck/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/web/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | . 2 | ddt<2 3 | flake8 4 | flask 5 | pytest-cov<4 6 | tornado 7 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.10.1 3 | 4 | [bumpversion:file:setup.py] 5 | -------------------------------------------------------------------------------- /py-healthcheck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ateliedocodigo/py-healthcheck/HEAD/py-healthcheck.jpg -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = src 4 | omit = .tox/*,*test_* 5 | 6 | [report] 7 | show_missing = True 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | 4 | [flake8] 5 | max-line-length = 120 6 | exclude = .tox,env,docs,venv 7 | 8 | [options.package_data] 9 | * = py.typed 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.{json,yml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /healthcheck/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | try: 5 | from functools import reduce # noqa 6 | except Exception: 7 | pass 8 | 9 | try: 10 | from .tornado_handler import TornadoHandler # noqa 11 | except ImportError: 12 | pass 13 | 14 | from .environmentdump import EnvironmentDump # noqa 15 | from .healthcheck import HealthCheck # noqa 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.xml 2 | .changelog 3 | 4 | *.py[cod] 5 | *.swp 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Packages 11 | *.egg 12 | *.egg-info 13 | dist 14 | build 15 | eggs 16 | parts 17 | bin 18 | var 19 | sdist 20 | develop-eggs 21 | .installed.cfg 22 | lib 23 | lib64 24 | env 25 | venv 26 | 27 | # Installer logs 28 | pip-log.txt 29 | 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .coverage 33 | .tox 34 | nosetests.xml 35 | 36 | # Translations 37 | *.mo 38 | 39 | # Mr Developer 40 | .mr.developer.cfg 41 | .project 42 | .pydevproject 43 | 44 | .idea 45 | 46 | .DS_Store 47 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: double-quote-string-fixer 7 | - id: end-of-file-fixer 8 | - id: check-yaml 9 | - id: check-json 10 | - id: check-added-large-files 11 | - id: name-tests-test 12 | - id: requirements-txt-fixer 13 | - repo: https://github.com/PyCQA/flake8 14 | rev: 4.0.1 15 | hooks: 16 | - id: flake8 17 | - repo: https://github.com/Yelp/detect-secrets 18 | rev: v1.2.0 19 | hooks: 20 | - id: detect-secrets 21 | -------------------------------------------------------------------------------- /healthcheck/tornado_handler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | try: 4 | from typing import Any 5 | except ImportError: 6 | # for python2 7 | pass 8 | 9 | import tornado.web 10 | 11 | from .healthcheck import HealthCheck 12 | 13 | 14 | class TornadoHandler(tornado.web.RequestHandler): 15 | 16 | def initialize(self, checker): # type: (HealthCheck) -> None 17 | self.checker = checker 18 | 19 | def get(self, *args, **kwargs): # type: (*Any, **Any) -> None 20 | message, status_code, headers = self.checker.run(*args, **kwargs) 21 | self.set_status(status_code) 22 | for k, v in headers.items(): 23 | self.set_header(k, v) 24 | self.write(message) 25 | -------------------------------------------------------------------------------- /healthcheck/timeout.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from functools import wraps 4 | import errno 5 | import os 6 | import signal 7 | 8 | 9 | class TimeoutError(Exception): 10 | pass 11 | 12 | 13 | def timeout(seconds=2, error_message=os.strerror( 14 | getattr(errno, 'ETIME', errno.ETIMEDOUT))): 15 | def decorator(func): 16 | def _handle_timeout(signum, frame): 17 | raise TimeoutError(error_message) 18 | 19 | def wrapper(*args, **kwargs): 20 | signal.signal(signal.SIGALRM, _handle_timeout) 21 | signal.alarm(seconds) 22 | try: 23 | result = func(*args, **kwargs) 24 | finally: 25 | signal.alarm(0) 26 | return result 27 | 28 | return wraps(func)(wrapper) 29 | 30 | return decorator 31 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | pre-commit: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-python@v3 15 | - uses: pre-commit/action@v3.0.0 16 | type-check: 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest] 20 | python-version: ["3.9"] 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v3 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Install tox 29 | run: pip install tox 30 | - name: Run static type check (MyPy) with tox 31 | run: tox -e mypy 32 | -------------------------------------------------------------------------------- /tests/unit/healthcheck_single_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import json 4 | import unittest 5 | 6 | from healthcheck import HealthCheck 7 | 8 | 9 | class HealthCheckSingleRunTest(unittest.TestCase): 10 | 11 | @staticmethod 12 | def check_that_works(): 13 | """Check that always return true.""" 14 | return True, 'it works' 15 | 16 | @staticmethod 17 | def check_throws_exception(): 18 | raise Exception('My exception') 19 | 20 | def test_should_run_only_filtered_checks(self): 21 | check = 'check_that_works' 22 | hc = HealthCheck(checkers=[self.check_that_works, self.check_throws_exception]) 23 | 24 | message, status, headers = hc.run(check) 25 | self.assertEqual(200, status) 26 | 27 | jr = json.loads(message) 28 | self.assertEqual('success', jr['status']) 29 | self.assertEqual(len(jr['results']), 1) 30 | 31 | 32 | if __name__ == '__main__': 33 | unittest.main() 34 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py3,flask,tornado,flake8,mypy 3 | 4 | [testenv] 5 | setenv = PYTHONDONTWRITEBYTECODE=1 6 | skip_missing_interpreters = true 7 | deps = -r requirements-dev.txt 8 | commands = py.test \ 9 | --cov=. \ 10 | --cov-report xml \ 11 | --cov-report html \ 12 | --doctest-modules \ 13 | --cov-report term 14 | 15 | [testenv:flake8] 16 | commands = flake8 17 | 18 | [testenv:mypy] 19 | deps = -r requirements-dev.txt 20 | mypy 21 | types-six 22 | commands = 23 | mypy healthcheck --show-error-codes 24 | mypy tests/unit --show-error-codes 25 | 26 | [testenv:outdated] 27 | commands = pip list --outdated 28 | 29 | [testenv:flask] 30 | setenv = PYTHONDONTWRITEBYTECODE=1 31 | deps = flask 32 | ; commands = python setup.py test 33 | commands = python -m unittest discover -v -p '*flask*' tests 34 | 35 | [testenv:tornado] 36 | setenv = PYTHONDONTWRITEBYTECODE=1 37 | deps = tornado 38 | ; commands = python setup.py test 39 | commands = python -m unittest discover -v -p '*tornado*' tests 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Runscope Inc. 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 | -------------------------------------------------------------------------------- /tests/web/flaskapp_test.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from flask import Flask 4 | 5 | from healthcheck import HealthCheck, EnvironmentDump 6 | 7 | app = Flask(__name__) 8 | 9 | health = HealthCheck() 10 | envdump = EnvironmentDump() 11 | 12 | 13 | # add your own check function to the healthcheck 14 | def redis_available(): 15 | time.sleep(3) 16 | return True, 'redis ok' 17 | 18 | 19 | def rest_available(): 20 | time.sleep(2) 21 | raise Exception('NO') 22 | # return True, "rest ok" 23 | 24 | 25 | health.add_check(redis_available) 26 | health.add_check(rest_available) 27 | 28 | 29 | # add your own data to the environment dump 30 | def application_data(): 31 | return {'maintainer': 'Luis Fernando Gomes', 32 | 'git_repo': 'https://github.com/ateliedocodigo/py-healthcheck'} 33 | 34 | 35 | envdump.add_section('application', application_data) 36 | 37 | # Add a flask route to expose information 38 | app.add_url_rule('/healthcheck', 'healthcheck', view_func=lambda: health.run()) 39 | app.add_url_rule('/environment', 'environment', view_func=lambda: envdump.run()) 40 | 41 | if __name__ == '__main__': 42 | app.run(host='0.0.0.0', port='5000') 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from setuptools import setup, find_packages 4 | 5 | __version__ = '1.10.1' 6 | __repo__ = 'https://github.com/ateliedocodigo/py-healthcheck' 7 | 8 | 9 | def read(fname): 10 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 11 | 12 | 13 | setup( 14 | name='py-healthcheck', 15 | version=__version__, 16 | description='Adds healthcheck endpoints to Flask or Tornado apps', 17 | long_description=read('README.rst'), 18 | author='Luis Fernando Gomes', 19 | author_email='luiscoms@ateliedocodigo.com.br', 20 | url=__repo__, 21 | download_url='{}/tarball/{}'.format(__repo__, __version__), 22 | packages=find_packages(exclude=['tests', 'tests.*']), 23 | zip_safe=False, 24 | include_package_data=True, 25 | license='MIT', 26 | platforms='any', 27 | install_requires=['six'], 28 | classifiers=[ 29 | 'Development Status :: 5 - Production/Stable', 30 | 'Environment :: Web Environment', 31 | 'Framework :: Flask', 32 | # "Framework :: Tornado", 33 | 'Programming Language :: Python :: 2.7', 34 | 'Programming Language :: Python :: 3', 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /healthcheck/security.py: -------------------------------------------------------------------------------- 1 | try: 2 | from typing import Any, Tuple 3 | except ImportError: 4 | # for python2 5 | pass 6 | try: 7 | from collections.abc import Mapping # only works on python 3.3+ 8 | except ImportError: 9 | from collections import Mapping # type: ignore[attr-defined, no-redef] 10 | 11 | 12 | def safe_dict(dictionary, blacklist=('key', 'token', 'pass'), max_deep=5): 13 | # type: (Mapping[str, Any], Tuple[str,...], int) -> Mapping[str, Any] 14 | """ Avoid listing passwords and access tokens or keys in the dictionary 15 | 16 | :param dictionary: Input dictionary 17 | :param blacklist: blacklist keys 18 | :param max_deep: Maximum dictionary dict iteration 19 | :return: Safe dictionary 20 | """ 21 | if max_deep <= 0: 22 | return dictionary 23 | result = {} 24 | for key in dictionary.keys(): 25 | if isinstance(dictionary[key], Mapping): 26 | result[key] = safe_dict(dictionary[key], blacklist, max_deep - 1) 27 | elif any(b in key.lower() for b in blacklist): 28 | result[key] = '********' # type:ignore[assignment] 29 | else: 30 | result[key] = dictionary[key] 31 | return result 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-python@v2 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install setuptools wheel twine 19 | - name: Build and publish 20 | env: 21 | TWINE_USERNAME: __token__ 22 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 23 | run: | 24 | python setup.py sdist bdist_wheel 25 | twine upload dist/* 26 | 27 | deploy-on-testpypi: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: actions/setup-python@v2 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install --upgrade pip 36 | pip install setuptools wheel twine 37 | - name: Build and publish 38 | env: 39 | TWINE_USERNAME: __token__ 40 | TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} 41 | TWINE_REPOSITORY: testpypi 42 | run: | 43 | python setup.py sdist bdist_wheel 44 | twine upload dist/* 45 | -------------------------------------------------------------------------------- /CHANGELOG-legacy.md: -------------------------------------------------------------------------------- 1 | # Healthcheck Changelog 2 | ### 1.7.2 3 | * Fix compatibility issue with FreeBSD 4 | 5 | ### 1.7.1 6 | 7 | * Change call of `os.uname()` to `platform.uname()`, fixes #13 8 | 9 | ### 1.7.0 10 | 11 | * Code checks with ``flake8`` instead of ``pep8``, fixes #9 12 | * Adds response_time to check output, closes #4 13 | 14 | ### 1.6.1 15 | - Change default timeout to 0 (no timeout). 16 | 17 | ### 1.6.0 18 | - Adds timeout on execution checkers. 19 | 20 | ### 1.5.0 21 | - Adds `HealthCheck.add_section` to increase output 22 | 23 | ### 1.4.0 24 | - Add tornado support. 25 | 26 | ### 1.3.1 27 | - Fix for 'Inappropriate ioctl for device' error on posix systems. 28 | 29 | ### 1.3.0 30 | - Adds support for init_app construction of healthcheck objects. Thanks to 31 | Iuri de Silvio for the pull request. 32 | 33 | ### 1.2.0 34 | - Adds support for Python 3.x. Thanks to Guilherme D'Amoreira for the pull 35 | request. 36 | 37 | ### 1.1.0 38 | - Added the `EnvironmentDump` class which provides a second endpoint for 39 | details about your application's environment. 40 | 41 | ### 1.0.0 42 | - Incremented the version number to indicate that this is a stable release. 43 | 44 | ### 0.2 45 | - Added caching to health check responses. Successful checks are cached for 27 46 | seconds; failures are cached for 9 seconds. 47 | - Removed the "simple" view of the health check, which had been available with 48 | the query string `?simple=true`. This isn't necessary now that we cache the 49 | results of the checks. 50 | - Added `timestamp` field to the outer level of the JSON response, and 51 | `timestamp` and `expires` to each check result. 52 | -------------------------------------------------------------------------------- /tests/unit/environmentdump_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import json 4 | import os 5 | import unittest 6 | 7 | from healthcheck import EnvironmentDump 8 | 9 | try: 10 | from collections.abc import Mapping # only works on python 3.3+ 11 | except ImportError: 12 | from collections import Mapping # type: ignore[attr-defined, no-redef] 13 | 14 | 15 | class BasicEnvironmentDumpTest(unittest.TestCase): 16 | 17 | def test_basic_check(self): 18 | def custom_section(): 19 | return 'My custom section' 20 | 21 | ed = EnvironmentDump() 22 | 23 | ed.add_section('custom_section', custom_section) 24 | 25 | message, status, headers = ed.run() 26 | 27 | jr = json.loads(message) 28 | self.assertEqual('My custom section', jr['custom_section']) 29 | 30 | def test_custom_section_signature(self): 31 | def custom_section(): 32 | return 'My custom section' 33 | 34 | ed = EnvironmentDump(custom_section=custom_section) 35 | 36 | message, status, headers = ed.run() 37 | 38 | jr = json.loads(message) 39 | self.assertEqual('My custom section', jr['custom_section']) 40 | 41 | 42 | class TestEnvironmentDumpSafeDump(unittest.TestCase): 43 | 44 | def test_should_return_safe_environment_vars(self): 45 | os.environ['SOME_KEY'] = 'fake-key' 46 | 47 | ed = EnvironmentDump() 48 | message, status, headers = ed.run() 49 | 50 | jr = json.loads(message) 51 | self.assertIsInstance(jr['process']['environ'], Mapping) 52 | self.assertEqual('********', jr['process']['environ']['SOME_KEY']) 53 | 54 | 55 | if __name__ == '__main__': 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run the test suite 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - develop 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test-for-python3: 12 | strategy: 13 | #fail-fast: false 14 | matrix: 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | python-version: ["3.5.10", "3.6.16", "3.7.15", "3.8.14", "3.9.15", "3.10.8"] 17 | exclude: 18 | - os: windows-latest 19 | python-version: "3.10" # won't compile psycopg2 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install tox 28 | run: pip install tox 29 | - name: Run unit tests with tox 30 | run: tox -e py3 31 | - name: Run flask test with tox 32 | run: tox -e flask 33 | - name: Run tornado test with tox 34 | run: tox -e tornado 35 | 36 | test-for-python2: 37 | strategy: 38 | #fail-fast: false 39 | matrix: 40 | os: [ubuntu-latest, macos-latest, windows-latest] 41 | python-version: ["2.7.18"] 42 | runs-on: ${{ matrix.os }} 43 | steps: 44 | - uses: actions/checkout@v3 45 | - name: Set up Python ${{ matrix.python-version }} 46 | uses: actions/setup-python@v3 47 | with: 48 | python-version: ${{ matrix.python-version }} 49 | - name: Install tox 50 | run: pip install tox 51 | - name: Run unit tests with tox 52 | run: tox -e py2 53 | - name: Run flask test with tox 54 | run: tox -e flask 55 | - name: Run tornado test with tox 56 | run: tox -e tornado 57 | -------------------------------------------------------------------------------- /tests/web/flask_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | 5 | import flask 6 | 7 | from healthcheck import HealthCheck, EnvironmentDump 8 | 9 | 10 | class BasicHealthCheckTest(unittest.TestCase): 11 | 12 | def setUp(self): 13 | self.path = '/h' 14 | self.app = flask.Flask(__name__) 15 | self.hc = self._hc() 16 | self.client = self.app.test_client() 17 | 18 | self.app.add_url_rule(self.path, view_func=lambda: self.hc.run()) 19 | 20 | def _hc(self): 21 | return HealthCheck() 22 | 23 | def test_basic_check(self): 24 | response = self.client.get(self.path) 25 | self.assertEqual(200, response.status_code) 26 | 27 | def test_failing_check(self): 28 | def fail_check(): 29 | return False, 'FAIL' 30 | 31 | self.hc.add_check(fail_check) 32 | response = self.client.get(self.path) 33 | self.assertEqual(500, response.status_code) 34 | 35 | jr = flask.json.loads(response.data) 36 | self.assertEqual('failure', jr['status']) 37 | 38 | 39 | class BasicEnvironmentDumpTest(unittest.TestCase): 40 | 41 | def setUp(self): 42 | self.path = '/e' 43 | self.app = flask.Flask(__name__) 44 | self.hc = self._hc() 45 | self.client = self.app.test_client() 46 | 47 | self.app.add_url_rule(self.path, view_func=lambda: self.hc.run()) 48 | 49 | def _hc(self): 50 | return EnvironmentDump() 51 | 52 | def test_basic_check(self): 53 | def test_ok(): 54 | return 'OK' 55 | 56 | self.hc.add_section('test_func', test_ok) 57 | self.hc.add_section('config', self.app.config) 58 | 59 | response = self.client.get(self.path) 60 | self.assertEqual(200, response.status_code) 61 | jr = flask.json.loads(response.data) 62 | self.assertEqual('OK', jr['test_func']) 63 | 64 | 65 | if __name__ == '__main__': 66 | unittest.main() 67 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PIP = pip3 2 | PYTHON = python3 3 | PIPY_REPOSITORY=pypi 4 | 5 | ifdef PY_VENV_PATH 6 | PYTHON_ACTIVATE = . $(PY_VENV_PATH)/bin/activate 7 | PIP = $(PYTHON_ACTIVATE) && pip 8 | PYTHON_BIN := $(PYTHON) 9 | PYTHON := $(PYTHON_ACTIVATE) && $(PYTHON) 10 | ifneq ("$(wildcard $(PY_VENV_PATH)/bin/activate)","") 11 | $(PYTHON_ACTIVATE): 12 | else 13 | $(PYTHON_ACTIVATE): 14 | virtualenv -p$(PYTHON_BIN) $(PY_VENV_PATH) 15 | endif 16 | endif 17 | 18 | .PHONY: clean 19 | clean: 20 | rm -rf .cache .tox/ .coverage build/ dist/ docs/_build htmlcov *.egg-info 21 | find . -name \*.pyc -delete -print 22 | find . -name __pycache__ -delete -print 23 | 24 | .PHONY: install 25 | install: $(PYTHON_ACTIVATE) 26 | # $(PIP) install -r test-requirements.txt 27 | $(PYTHON) setup.py install 28 | 29 | .PHONY: install-dev 30 | install-dev: $(PYTHON_ACTIVATE) 31 | $(PIP) install -r test-requirements.txt 32 | # $(PYTHON) setup.py install 33 | 34 | # make lint PY_VENV_PATH=env 35 | .PHONY: lint 36 | lint: $(PYTHON_ACTIVATE) install-dev 37 | echo "Start linting" 38 | $(PYTHON_ACTIVATE) && flake8 39 | # $(PYTHON_ACTIVATE) && pep257 boilerplate tests 40 | 41 | # make test PY_VENV_PATH=env 42 | .PHONY: test 43 | test: install-dev lint 44 | echo "Start testing" 45 | $(PYTHON_ACTIVATE) && python setup.py test 46 | 47 | # make register PIPY_REPOSITORY=pypitest 48 | .PHONY: register 49 | register: 50 | $(PYTHON) setup.py register -r ${PIPY_REPOSITORY} 51 | 52 | # make dist PIPY_REPOSITORY=pypitest 53 | .PHONY: dist 54 | dist: install 55 | $(PYTHON) setup.py sdist 56 | 57 | # make upload PIPY_REPOSITORY=pypitest 58 | .PHONY: upload 59 | upload: $(PYTHON_ACTIVATE) 60 | $(PYTHON) setup.py sdist upload -r ${PIPY_REPOSITORY} 61 | 62 | # make minor PY_VENV_PATH=env 63 | .PHONY: minor 64 | minor: 65 | $(PYTHON_ACTIVATE) && bumpversion --allow-dirty --verbose minor 66 | 67 | # make patch PY_VENV_PATH=env 68 | .PHONY: patch 69 | patch: 70 | $(PYTHON_ACTIVATE) && bumpversion --allow-dirty --verbose patch 71 | 72 | ifndef VERBOSE 73 | .SILENT: 74 | endif 75 | -------------------------------------------------------------------------------- /tests/web/tornado_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import json 4 | import unittest 5 | 6 | import tornado.testing 7 | import tornado.web 8 | from tornado.testing import AsyncHTTPTestCase 9 | 10 | from healthcheck import TornadoHandler, HealthCheck, EnvironmentDump 11 | 12 | 13 | class BasicHealthCheckTest(AsyncHTTPTestCase): 14 | def get_app(self): 15 | return tornado.web.Application() 16 | 17 | def setUp(self): 18 | super(BasicHealthCheckTest, self).setUp() 19 | self.path = '/h' 20 | self.hc = self._hc() 21 | self._set_handler() 22 | 23 | def _hc(self): 24 | return HealthCheck() 25 | 26 | def _set_handler(self): 27 | 28 | handler_args = dict(checker=self.hc) 29 | self._app.add_handlers( 30 | r'.*', 31 | [ 32 | ( 33 | self.path, 34 | TornadoHandler, handler_args 35 | ), 36 | ] 37 | ) 38 | 39 | def test_basic_check(self): 40 | 41 | response = self.fetch(self.path) 42 | self.assertEqual(response.code, 200) 43 | 44 | def test_failing_check(self): 45 | def fail_check(): 46 | return False, 'FAIL' 47 | 48 | self.hc.add_check(fail_check) 49 | response = self.fetch(self.path) 50 | self.assertEqual(response.code, 500) 51 | 52 | jr = json.loads(response.body.decode('ascii')) 53 | self.assertEqual('failure', jr['status']) 54 | 55 | 56 | class BasicEnvironmentDumpTest(AsyncHTTPTestCase): 57 | 58 | def get_app(self): 59 | return tornado.web.Application() 60 | 61 | def setUp(self): 62 | super(BasicEnvironmentDumpTest, self).setUp() 63 | self.path = '/e' 64 | self.hc = self._hc() 65 | 66 | def _hc(self): 67 | return EnvironmentDump() 68 | 69 | def _set_handler(self): 70 | 71 | handler_args = dict(checker=self.hc) 72 | self._app.add_handlers( 73 | r'.*', 74 | [ 75 | ( 76 | self.path, 77 | TornadoHandler, handler_args 78 | ), 79 | ] 80 | ) 81 | 82 | def test_basic_check(self): 83 | def test_ok(): 84 | return 'OK' 85 | 86 | self.hc.add_section('test_func', test_ok) 87 | self._set_handler() 88 | 89 | response = self.fetch(self.path) 90 | self.assertEqual(response.code, 200) 91 | jr = json.loads(response.body.decode('ascii')) 92 | self.assertEqual('OK', jr['test_func']) 93 | 94 | 95 | if __name__ == '__main__': 96 | unittest.main() 97 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Project 2 | 3 | ## Development workflow with Git 4 | 5 | ### Fork, Branching, Commits, and Pull Request 6 | 7 | 1. [Fork a repo](http://help.github.com/fork-a-repo/) **ateliedocodigo/py-healthcheck**. 8 | 9 | 2. Clone the **py-healthcheck** project to your local machine (**username** - your Github user account name.): 10 | ``` 11 | $ git clone git@github.com:USERNAME/py-healthcheck.git 12 | ``` 13 | 3. Configure remotes: 14 | ``` 15 | $ cd py-healthcheck 16 | $ git remote add upstream git@github.com:ateliedocodigo/py-healthcheck.git 17 | ``` 18 | 4. Create a branch for new check: 19 | ``` 20 | $ git checkout -b my-new-check 21 | ``` 22 | 5. Develop on **my-new-check** branch only, but **Do not merge my-new-check branch to the your develop (as it should stay equal to upstream develop)!!** 23 | 24 | 6. Commit changes to **my-new-check** branch: 25 | ``` 26 | $ git add . 27 | $ git commit -m "commit message" 28 | ``` 29 | 30 | 6.1. Update `CHANGELOG.md` 31 | 32 | Example: 33 | ``` 34 | ### Next Release 35 | 36 | * My changes closes #1 37 | ``` 38 | 39 | 7. Push branch to GitHub, to allow your mentor to review your code: 40 | ``` 41 | $ git push origin my-new-check 42 | ``` 43 | 8. Repeat steps 5-7 till development is complete. 44 | 45 | 9. Fetch upstream changes that were done by other contributors: 46 | ``` 47 | $ git fetch upstream 48 | ``` 49 | 10. Update local develop branch: 50 | ``` 51 | $ git checkout develop 52 | $ git pull upstream develop 53 | ``` 54 | 55 | ATTENTION: any time you lost of track of your code - launch "gitk --all" in source folder, UI application come up that will show all branches and history in pretty view, [explanation](http://lostechies.com/joshuaflanagan/2010/09/03/use-gitk-to-understand-git/). 56 | 57 | 11. Rebase **my-new-check** branch on top of the upstream master: 58 | ``` 59 | $ git checkout my-new-check 60 | $ git rebase master 61 | ``` 62 | 12. In the process of the **rebase**, it may discover conflicts. In that case it will stop and allow you to fix the conflicts. After fixing conflicts, use **git add .** to update the index with those contents, and then just run: 63 | ``` 64 | $ git rebase --continue 65 | ``` 66 | 13. Push branch to GitHub (with all your final changes and actual code of project): 67 | We forcing changes to your issue branch(our sand box) is not common branch, and rebasing means recreation of commits so no way to push without force. NEVER force to common branch. 68 | ``` 69 | $ git push origin my-new-check --force 70 | ``` 71 | 72 | 14. Created build for testing and send it to any mentor for testing. 73 | 74 | 15. Only after all testing is done - Send a [Pull Request](http://help.github.com/send-pull-requests/). 75 | Attention: Please recheck that in your pull request you send only your changes, and no other changes!! 76 | Check it by command: 77 | ``` 78 | git diff my-new-check upstream/develop 79 | ``` 80 | More detailed information you can find on [Git-rebase (Manual Page)](http://kernel.org/pub/software/scm/git/docs/git-rebase.html) and [Rebasing](http://git-scm.com/book/en/v2/Git-Branching-Rebasing). 81 | 82 | ## Running tests 83 | 84 | Install dependencies 85 | ``` 86 | $ pip install tox 87 | ``` 88 | 89 | Run tests! 90 | ``` 91 | $ tox 92 | ``` 93 | -------------------------------------------------------------------------------- /healthcheck/environmentdump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import json 4 | import os 5 | import platform 6 | import sys 7 | try: 8 | from typing import Dict, Any, Tuple, Callable 9 | except ImportError: 10 | # for python2 11 | pass 12 | import six 13 | 14 | from .security import safe_dict 15 | 16 | 17 | class EnvironmentDump: 18 | def __init__(self, 19 | include_os=True, 20 | include_python=True, 21 | include_process=True, 22 | **kwargs): 23 | self.functions = {} 24 | if include_os: 25 | self.functions['os'] = self.get_os 26 | if include_python: 27 | self.functions['python'] = self.get_python 28 | if include_process: 29 | self.functions['process'] = self.get_process 30 | 31 | # ads custom_sections on signature 32 | for k, v in kwargs.items(): 33 | if k not in self.functions: 34 | self.add_section(k, v) 35 | 36 | def add_section(self, name, func): # type: (Any, Callable) -> None 37 | if name in self.functions: 38 | raise Exception('The name "{}" is already taken.'.format(name)) 39 | if not hasattr(func, '__call__'): 40 | self.functions[name] = lambda: func 41 | return 42 | self.functions[name] = func 43 | 44 | def run(self): # type: () -> Tuple[str, int, Dict[str, str]] 45 | data = {} 46 | for (name, func) in six.iteritems(self.functions): 47 | data[name] = func() 48 | 49 | return json.dumps(data, default=str), 200, {'Content-Type': 'application/json'} 50 | 51 | def get_os(self): # type: () -> Dict[str, Any] 52 | return {'platform': sys.platform, 53 | 'name': os.name, 54 | 'uname': platform.uname()} 55 | 56 | def get_python(self): # type: () -> Dict[str, Any] 57 | result = {'version': sys.version, 58 | 'executable': sys.executable, 59 | 'pythonpath': sys.path, 60 | 'version_info': {'major': sys.version_info.major, 61 | 'minor': sys.version_info.minor, 62 | 'micro': sys.version_info.micro, 63 | 'releaselevel': sys.version_info.releaselevel, 64 | 'serial': sys.version_info.serial}} 65 | try: 66 | import pip 67 | packages = {p.project_name: p.version 68 | for p in pip.get_installed_distributions()} # type:ignore[attr-defined] 69 | result['packages'] = packages 70 | except AttributeError: 71 | pass 72 | 73 | return result 74 | 75 | def get_login(self): # type: () -> str 76 | # Based on https://github.com/gitpython-developers/GitPython/pull/43/ 77 | # Fix for 'Inappopropirate ioctl for device' on posix systems. 78 | if os.name == 'posix': 79 | import pwd 80 | username = pwd.getpwuid(os.geteuid()).pw_name # type:ignore[attr-defined] 81 | else: 82 | username = os.environ.get('USER', os.environ.get('USERNAME', 'UNKNOWN')) 83 | if username == 'UNKNOWN' and hasattr(os, 'getlogin'): 84 | username = os.getlogin() 85 | return username 86 | 87 | def get_process(self): # type:() -> Dict[str, Any] 88 | return {'argv': sys.argv, 89 | 'cwd': os.getcwd(), 90 | 'user': self.get_login(), 91 | 'pid': os.getpid(), 92 | 'environ': safe_dict(os.environ)} 93 | -------------------------------------------------------------------------------- /tests/unit/security_test.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from unittest import TestCase 3 | 4 | from ddt import data, ddt, unpack # type:ignore[import] 5 | 6 | from healthcheck.security import safe_dict 7 | 8 | 9 | def make_test_dict(test_key, test_value, deep=1): 10 | if deep > 1: 11 | return dict(dummy=make_test_dict(test_key, test_value, deep - 1)) 12 | return {test_key: test_value} 13 | 14 | 15 | class MakeTestDictTest(TestCase): 16 | 17 | def test_should_return_input_value_if_deep_is_lte_0(self): 18 | self.assertEqual({'a': 'asd'}, make_test_dict('a', 'asd', 0)) 19 | 20 | def test_should_make_test_dict_first_level(self): 21 | self.assertEqual({'a': 'asd'}, make_test_dict('a', 'asd')) 22 | self.assertEqual({'a': '***'}, make_test_dict('a', '***')) 23 | 24 | def test_should_make_test_dict_second_level(self): 25 | self.assertEqual({'dummy': {'a': 'asd'}}, make_test_dict('a', 'asd', 2)) 26 | self.assertEqual({'dummy': {'a': '***'}}, make_test_dict('a', '***', 2)) 27 | 28 | def test_should_make_test_dict_third_level(self): 29 | self.assertEqual({'dummy': {'dummy': {'a': 'asd'}}}, make_test_dict('a', 'asd', 3)) 30 | self.assertEqual({'dummy': {'dummy': {'a': '***'}}}, make_test_dict('a', '***', 3)) 31 | 32 | 33 | @ddt 34 | class SafeDictTest(TestCase): 35 | 36 | def test_should_return_input_value_if_max_deep_is_lte_0(self): 37 | self.assertEqual({'a': 'asd'}, safe_dict({'a': 'asd'}, max_deep=0)) 38 | 39 | @unpack 40 | @data( 41 | (1, 'a', 'asdf'), 42 | (1, 'a', 42), 43 | (1, 'a', 3.14), 44 | (1, 'a', datetime.now()), 45 | ) 46 | def test_should_dump_dictionary_without_blacklisted_keys(self, deep, key_to_test, value): 47 | input_dict = make_test_dict(key_to_test, value, deep) 48 | to_dict = safe_dict(input_dict) 49 | 50 | self.assertEqual(input_dict, to_dict) 51 | 52 | @unpack 53 | @data( 54 | (1, 'key', 'asdf'), 55 | (1, 'Key', 'asdf'), 56 | (1, 'somekey', 'asdf'), 57 | (1, 'someKey', 'asdf'), 58 | (1, 'key_value', 'asdf'), 59 | (1, 'Key_value', 'asdf'), 60 | 61 | (1, 'key', 42), 62 | (1, 'Key', 42), 63 | (1, 'somekey', 42), 64 | (1, 'someKey', 42), 65 | (1, 'key_value', 42), 66 | (1, 'Key_value', 42), 67 | 68 | (1, 'key', 3.14), 69 | (1, 'Key', 3.14), 70 | (1, 'somekey', 3.14), 71 | (1, 'someKey', 3.14), 72 | (1, 'key_value', 3.14), 73 | (1, 'Key_value', 3.14), 74 | 75 | (1, 'key', datetime.now()), 76 | (1, 'Key', datetime.now()), 77 | (1, 'somekey', datetime.now()), 78 | (1, 'someKey', datetime.now()), 79 | (1, 'key_value', datetime.now()), 80 | (1, 'Key_value', datetime.now()), 81 | 82 | (2, 'key', 'asdf'), 83 | (2, 'Key', 'asdf'), 84 | (2, 'somekey', 'asdf'), 85 | (2, 'someKey', 'asdf'), 86 | (2, 'key_value', 'asdf'), 87 | (2, 'Key_value', 'asdf'), 88 | 89 | (3, 'key', 'asdf'), 90 | (3, 'Key', 'asdf'), 91 | (3, 'somekey', 'asdf'), 92 | (3, 'someKey', 'asdf'), 93 | (3, 'key_value', 'asdf'), 94 | (3, 'Key_value', 'asdf'), 95 | 96 | (4, 'key', 'asdf'), 97 | (4, 'Key', 'asdf'), 98 | (4, 'somekey', 'asdf'), 99 | (4, 'someKey', 'asdf'), 100 | (4, 'key_value', 'asdf'), 101 | (4, 'Key_value', 'asdf'), 102 | 103 | (5, 'key', 'asdf'), 104 | (5, 'Key', 'asdf'), 105 | (5, 'somekey', 'asdf'), 106 | (5, 'someKey', 'asdf'), 107 | (5, 'key_value', 'asdf'), 108 | (5, 'Key_value', 'asdf'), 109 | ) 110 | def test_should_dump_dictionary_with_blacklisted_key_deep(self, deep, key_to_test, value): 111 | input_dict = make_test_dict(key_to_test, value, deep) 112 | to_dict = safe_dict(input_dict) 113 | 114 | self.assertEqual(make_test_dict(key_to_test, '********', deep), to_dict) 115 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | All releases are documented in this file. 5 | Details about each change can be checked directly in the description of each merge request. 6 | 7 | Automatically generated by [`awesome-release`](https://github.com/rbsdev/awesome-release). 8 | 9 | ## [1.10.1](https://github.com/ateliedocodigo/py-healthcheck/compare/1.10.0...1.10.1) 10 | 11 | > 20 May 2020 12 | 13 | - Fix environment dump [`#24`](https://github.com/ateliedocodigo/py-healthcheck/pull/24) 14 | 15 | ## [1.10.0](https://github.com/ateliedocodigo/py-healthcheck/compare/1.9.0...1.10.0) 16 | 17 | > 15 May 2020 18 | 19 | - Improve safe dump [`#23`](https://github.com/ateliedocodigo/py-healthcheck/pull/23) 20 | 21 | ## [1.9.0](https://github.com/ateliedocodigo/py-healthcheck/compare/1.8.1...1.9.0) 22 | 23 | > 3 June 2019 24 | 25 | - Add single run [`#18`](https://github.com/ateliedocodigo/py-healthcheck/pull/18) 26 | 27 | ## [1.8.1](https://github.com/ateliedocodigo/py-healthcheck/compare/1.8.0...1.8.1) 28 | 29 | > 17 May 2019 30 | 31 | - Change logging to legger [`#17`](https://github.com/ateliedocodigo/py-healthcheck/pull/17) 32 | - Improve test matrix [`#16`](https://github.com/ateliedocodigo/py-healthcheck/pull/16) 33 | 34 | ## [1.8.0](https://github.com/ateliedocodigo/py-healthcheck/compare/1.7.2...1.8.0) 35 | 36 | > 15 April 2019 37 | 38 | - Supports section as value [`#15`](https://github.com/ateliedocodigo/py-healthcheck/pull/15) 39 | 40 | ## [1.7.2](https://github.com/ateliedocodigo/py-healthcheck/compare/1.7.1...1.7.2) 41 | 42 | > 18 September 2018 43 | 44 | - Fix: AttributeError on FreeBSD where errno.ETIME doesn't exist [`#14`](https://github.com/ateliedocodigo/py-healthcheck/pull/14) 45 | 46 | ## [1.7.1](https://github.com/ateliedocodigo/py-healthcheck/compare/1.7.0...1.7.1) 47 | 48 | > 20 July 2018 49 | 50 | - Fix tox tests [`#11`](https://github.com/ateliedocodigo/py-healthcheck/pull/11) 51 | 52 | ## [1.7.0](https://github.com/ateliedocodigo/py-healthcheck/compare/1.6.1...1.7.0) 53 | 54 | > 1 November 2017 55 | 56 | - Run flake8 instead of PEP8 [`#10`](https://github.com/ateliedocodigo/py-healthcheck/pull/10) 57 | - Add response_time to health checks [`#5`](https://github.com/ateliedocodigo/py-healthcheck/pull/5) 58 | 59 | ## [1.6.1](https://github.com/ateliedocodigo/py-healthcheck/compare/1.6.0...1.6.1) 60 | 61 | > 4 April 2017 62 | 63 | ## [1.6.0](https://github.com/ateliedocodigo/py-healthcheck/compare/1.5.0...1.6.0) 64 | 65 | > 31 March 2017 66 | 67 | - Feature timeout [`#3`](https://github.com/ateliedocodigo/py-healthcheck/pull/3) 68 | 69 | ## [1.5.0](https://github.com/ateliedocodigo/py-healthcheck/compare/1.4.0...1.5.0) 70 | 71 | > 17 February 2017 72 | 73 | ## [1.4.0](https://github.com/ateliedocodigo/py-healthcheck/compare/1.3.1...1.4.0) 74 | 75 | > 24 January 2017 76 | 77 | - Tornado support [`#1`](https://github.com/ateliedocodigo/py-healthcheck/pull/1) 78 | - Updated EnvironmentDump to support Python 3 [`#23`](https://github.com/ateliedocodigo/py-healthcheck/pull/23) 79 | 80 | ## [1.3.1](https://github.com/ateliedocodigo/py-healthcheck/compare/1.2.0...1.3.1) 81 | 82 | > 9 January 2016 83 | 84 | - Fix for 'Inappropriate ioctl for device' [`#19`](https://github.com/ateliedocodigo/py-healthcheck/pull/19) 85 | - Improving health code with landscape [`#15`](https://github.com/ateliedocodigo/py-healthcheck/pull/15) 86 | - Init app [`#17`](https://github.com/ateliedocodigo/py-healthcheck/pull/17) 87 | 88 | ## [1.2.0](https://github.com/ateliedocodigo/py-healthcheck/compare/1.1.0...1.2.0) 89 | 90 | > 26 November 2015 91 | 92 | - Changed to compatibility with python 3.x [`#12`](https://github.com/ateliedocodigo/py-healthcheck/pull/12) 93 | 94 | ## [1.1.0](https://github.com/ateliedocodigo/py-healthcheck/compare/1.0.0...1.1.0) 95 | 96 | > 26 April 2015 97 | 98 | - Fix formatting [`#8`](https://github.com/ateliedocodigo/py-healthcheck/pull/8) 99 | - Add a second endpoint to show info about your application's environment. [`#6`](https://github.com/ateliedocodigo/py-healthcheck/pull/6) 100 | 101 | ## [1.0.0](https://github.com/ateliedocodigo/py-healthcheck/compare/v0.1.1...1.0.0) 102 | 103 | > 5 November 2014 104 | 105 | - The beginning of a hoped for pypi distribution [`#5`](https://github.com/ateliedocodigo/py-healthcheck/pull/5) 106 | - Add caching to Healthcheck results [`#3`](https://github.com/ateliedocodigo/py-healthcheck/pull/3) 107 | - Add simple mode for healthchecks [`#2`](https://github.com/ateliedocodigo/py-healthcheck/pull/2) 108 | - Record failed health checks in app.logger. [`#1`](https://github.com/ateliedocodigo/py-healthcheck/pull/1) 109 | 110 | ## v0.1.1 111 | 112 | > 6 June 2013 113 | -------------------------------------------------------------------------------- /healthcheck/healthcheck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import json 4 | import logging 5 | import socket 6 | import time 7 | try: 8 | from typing import Any, Callable, Dict, Mapping, Optional, Tuple, Union 9 | except ImportError: 10 | # for python2 11 | pass 12 | 13 | import six 14 | 15 | from .timeout import timeout 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | try: 20 | from functools import reduce 21 | except Exception: 22 | pass 23 | 24 | 25 | def basic_exception_handler(_, exc): # type: (Any, Exception) -> Tuple[bool, str] 26 | return False, str(exc) 27 | 28 | 29 | def json_success_handler(results, *args, **kw): # type: (dict,*Any,**Any) -> str 30 | data = { 31 | 'hostname': socket.gethostname(), 32 | 'status': 'success', 33 | 'timestamp': time.time(), 34 | 'results': results, 35 | } 36 | data.update(kw) 37 | return json.dumps(data) 38 | 39 | 40 | def json_failed_handler(results, *args, **kw): # type: (dict, *Any, **Any) -> str 41 | data = { 42 | 'hostname': socket.gethostname(), 43 | 'status': 'failure', 44 | 'timestamp': time.time(), 45 | 'results': results, 46 | } 47 | data.update(kw) 48 | return json.dumps(data) 49 | 50 | 51 | def check_reduce(passed, result): # type: (bool, Mapping[str,bool]) -> bool 52 | return passed and result.get('passed') # type: ignore[return-value] 53 | 54 | 55 | class HealthCheck: 56 | def __init__(self, success_status=200, 57 | success_headers=None, success_handler=json_success_handler, 58 | success_ttl=27, failed_status=500, failed_headers=None, 59 | failed_handler=json_failed_handler, failed_ttl=9, 60 | error_timeout=0, 61 | exception_handler=basic_exception_handler, checkers=None, 62 | **kwargs): 63 | self.cache = dict() 64 | 65 | self.success_status = success_status 66 | self.success_headers = success_headers or {'Content-Type': 'application/json'} 67 | self.success_handler = success_handler 68 | self.success_ttl = float(success_ttl or 0) 69 | 70 | self.failed_status = failed_status 71 | self.failed_headers = failed_headers or {'Content-Type': 'application/json'} 72 | self.failed_handler = failed_handler 73 | self.failed_ttl = float(failed_ttl or 0) 74 | 75 | self.error_timeout = error_timeout 76 | 77 | self.exception_handler = exception_handler 78 | 79 | self.checkers = checkers or [] 80 | 81 | self.functions = dict() 82 | # ads custom_sections on signature 83 | for k, v in kwargs.items(): 84 | if k not in self.functions: 85 | self.add_section(k, v) 86 | 87 | def add_section(self, name, func): # type:(str, Callable[..., Tuple[bool,str]]) -> None 88 | if name in self.functions: 89 | raise Exception('The name "{}" is already taken.'.format(name)) 90 | self.functions[name] = func 91 | 92 | def add_check(self, func): # type:(Callable[..., Tuple[bool,str]]) -> None 93 | self.checkers.append(func) 94 | 95 | def run(self, check=None): # type:(Optional[Callable[..., Tuple[bool,str]]]) -> Tuple[str, int, Dict[str, str]] 96 | results = [] 97 | filtered = [c for c in self.checkers if check is None or c.__name__ == check] 98 | for checker in filtered: 99 | if checker in self.cache and self.cache[checker].get('expires') >= time.time(): 100 | result = self.cache[checker] 101 | else: 102 | result = self.run_check(checker) 103 | self.cache[checker] = result 104 | results.append(result) 105 | 106 | custom_section = dict() 107 | for (name, func) in six.iteritems(self.functions): 108 | try: 109 | custom_section[name] = func() if callable(func) else func 110 | except Exception: 111 | pass 112 | 113 | passed = reduce(check_reduce, results, True) 114 | 115 | if passed: 116 | message = 'OK' 117 | if self.success_handler: 118 | message = self.success_handler(results, **custom_section) 119 | 120 | return message, self.success_status, self.success_headers 121 | message = 'NOT OK' 122 | if self.failed_handler: 123 | message = self.failed_handler(results, **custom_section) 124 | return message, self.failed_status, self.failed_headers 125 | 126 | def run_check(self, checker): # type:(Callable) -> Dict[str, Union[str, float, bool]] 127 | start_time = time.time() 128 | 129 | try: 130 | if self.error_timeout > 0: 131 | passed, output = timeout(self.error_timeout, 'Timeout error!')(checker)() 132 | else: 133 | passed, output = checker() 134 | except Exception as exc: 135 | logger.error(exc) 136 | passed, output = self.exception_handler(checker, exc) 137 | 138 | end_time = time.time() 139 | elapsed_time = end_time - start_time 140 | # Reduce to 6 decimal points to have consistency with timestamp 141 | elapsed_time = float('{:.6f}'.format(elapsed_time)) 142 | 143 | if passed: 144 | msg = 'Health check "{}" passed'.format(checker.__name__) 145 | logger.debug(msg) 146 | else: 147 | msg = 'Health check "{}" failed with output "{}"'.format(checker.__name__, output) 148 | logger.error(msg) 149 | 150 | timestamp = time.time() 151 | if passed: 152 | expires = timestamp + self.success_ttl 153 | else: 154 | expires = timestamp + self.failed_ttl 155 | 156 | result = {'checker': checker.__name__, 157 | 'output': output, 158 | 'passed': passed, 159 | 'timestamp': timestamp, 160 | 'expires': expires, 161 | 'response_time': elapsed_time} 162 | return result 163 | -------------------------------------------------------------------------------- /tests/unit/healthcheck_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import json 3 | import unittest 4 | from sys import version_info 5 | 6 | from healthcheck import HealthCheck 7 | from healthcheck.healthcheck import json_success_handler 8 | 9 | 10 | class BasicHealthCheckTest(unittest.TestCase): 11 | 12 | @staticmethod 13 | def check_that_works(): 14 | """Check that always return true.""" 15 | return True, 'it works' 16 | 17 | @staticmethod 18 | def check_throws_exception(): 19 | bad_var = None 20 | bad_var['explode'] 21 | 22 | def test_basic_check(self): 23 | message, status, headers = HealthCheck().run() 24 | self.assertEqual(200, status) 25 | 26 | def test_failing_check(self): 27 | hc = HealthCheck(checkers=[self.check_throws_exception]) 28 | message, status, headers = hc.run() 29 | self.assertEqual(500, status) 30 | jr = json.loads(message) 31 | self.assertEqual('failure', jr['status']) 32 | 33 | def test_success_check(self): 34 | hc = HealthCheck(checkers=[self.check_that_works]) 35 | if version_info >= (3, 4): 36 | with self.assertLogs('healthcheck', level='DEBUG') as cm: 37 | message, status, headers = hc.run() 38 | self.assertEqual(cm.output, ['DEBUG:healthcheck.healthcheck:Health check "check_that_works" passed']) 39 | else: 40 | message, status, headers = hc.run() 41 | self.assertEqual(200, status) 42 | jr = json.loads(message) 43 | self.assertEqual('success', jr['status']) 44 | 45 | def test_custom_section_function_success_check(self): 46 | hc = HealthCheck(checkers=[self.check_that_works]) 47 | hc.add_section('custom_section', lambda: 'My custom section') 48 | message, status, headers = hc.run() 49 | self.assertEqual(200, status) 50 | jr = json.loads(message) 51 | self.assertEqual('My custom section', jr['custom_section']) 52 | 53 | def test_custom_section_signature_function_success_check(self): 54 | hc = HealthCheck(checkers=[self.check_that_works], custom_section=lambda: 'My custom section') 55 | message, status, headers = hc.run() 56 | self.assertEqual(200, status) 57 | jr = json.loads(message) 58 | self.assertEqual('My custom section', jr['custom_section']) 59 | 60 | def test_custom_section_value_success_check(self): 61 | hc = HealthCheck(checkers=[self.check_that_works]) 62 | hc.add_section('custom_section', 'My custom section') 63 | message, status, headers = hc.run() 64 | self.assertEqual(200, status) 65 | jr = json.loads(message) 66 | self.assertEqual('My custom section', jr['custom_section']) 67 | 68 | def test_custom_section_signature_value_success_check(self): 69 | hc = HealthCheck(checkers=[self.check_that_works], custom_section='My custom section') 70 | message, status, headers = hc.run() 71 | self.assertEqual(200, status) 72 | jr = json.loads(message) 73 | self.assertEqual('My custom section', jr['custom_section']) 74 | 75 | def test_custom_section_function_failing_check(self): 76 | hc = HealthCheck(checkers=[self.check_throws_exception]) 77 | hc.add_section('custom_section', lambda: 'My custom section') 78 | message, status, headers = hc.run() 79 | self.assertEqual(500, status) 80 | jr = json.loads(message) 81 | self.assertEqual('My custom section', jr['custom_section']) 82 | 83 | def test_custom_section_signature_function_failure_check(self): 84 | hc = HealthCheck(checkers=[self.check_throws_exception], custom_section=lambda: 'My custom section') 85 | message, status, headers = hc.run() 86 | self.assertEqual(500, status) 87 | jr = json.loads(message) 88 | self.assertEqual('My custom section', jr['custom_section']) 89 | 90 | def test_custom_section_value_failing_check(self): 91 | hc = HealthCheck(checkers=[self.check_throws_exception]) 92 | hc.add_section('custom_section', 'My custom section') 93 | message, status, headers = hc.run() 94 | self.assertEqual(500, status) 95 | jr = json.loads(message) 96 | self.assertEqual('My custom section', jr['custom_section']) 97 | 98 | def test_custom_section_signature_value_failing_check(self): 99 | hc = HealthCheck(checkers=[self.check_throws_exception], custom_section='My custom section') 100 | message, status, headers = hc.run() 101 | self.assertEqual(500, status) 102 | jr = json.loads(message) 103 | self.assertEqual('My custom section', jr['custom_section']) 104 | 105 | def test_custom_section_prevent_duplication(self): 106 | hc = HealthCheck(checkers=[self.check_that_works], custom_section='My custom section') 107 | self.assertRaises(Exception, 'The name "custom_section" is already taken.', 108 | hc.add_section, 'custom_section', 'My custom section') 109 | 110 | 111 | class TimeoutHealthCheckTest(unittest.TestCase): 112 | 113 | def test_default_timeout_should_success_check(self): 114 | def timeout_check(): 115 | import time 116 | time.sleep(10) 117 | return True, 'Waited for 10 seconds' 118 | 119 | hc = HealthCheck(checkers=[timeout_check]) 120 | message, status, headers = hc.run() 121 | self.assertEqual(200, status) 122 | jr = json.loads(message) 123 | self.assertEqual('success', jr['status']) 124 | 125 | def test_error_timeout_function_should_failing_check(self): 126 | def timeout_check(): 127 | import time 128 | time.sleep(5) 129 | return True, 'Waited for 10 seconds' 130 | 131 | hc = HealthCheck(checkers=[timeout_check], error_timeout=2) 132 | message, status, headers = hc.run() 133 | self.assertEqual(500, status) 134 | jr = json.loads(message) 135 | self.assertEqual('failure', jr['status']) 136 | 137 | def test_json_success_handler(self): 138 | input_data = { 139 | 'foo': 'bar', 140 | 'asd': 'yxc' 141 | } 142 | actual_string = json_success_handler(input_data) 143 | actual_dict = json.loads(actual_string) 144 | self.assertIn('status', actual_dict) 145 | self.assertIn('status', actual_dict) 146 | self.assertIn('timestamp', actual_dict) 147 | self.assertEqual(actual_dict['results'], input_data) 148 | 149 | 150 | if __name__ == '__main__': 151 | unittest.main() 152 | -------------------------------------------------------------------------------- /openapi/openapi-healthcheck.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: Health Check And Environment Dump 4 | version: 0.0.1 5 | description: | 6 | This OpenAPI Specification document contains 7 | the Healthcheck API. Healthcheck is 8 | built using py-healthcheck package 9 | from PyPi. 10 | contact: 11 | name: Ateliê do Código 12 | url: https://github.com/ateliedocodigo/py-healthcheck 13 | license: 14 | name: The MIT License (MIT) 15 | paths: 16 | /healthcheck: 17 | get: 18 | summary: | 19 | Health check for Python Flask or Tornade app. 20 | responses: 21 | '200': 22 | description: OK 23 | content: 24 | application/json: 25 | schema: 26 | $ref: '#/components/schemas/HealthResponse' 27 | example: | 28 | { 29 | "hostname": "d122bb054e3d", 30 | "status": "success", 31 | "timestamp": 1598022539.1783307, 32 | "results": [ 33 | { 34 | "checker": "check_is_up", 35 | "output": "UP", 36 | "passed": true, 37 | "timestamp": 1598022539.1783178, 38 | "expires": 1598022566.1783178 39 | } 40 | ] 41 | } 42 | /environment: 43 | get: 44 | summary: | 45 | Environment dump for Python Flask or Tornado app. 46 | responses: 47 | '200': 48 | description: OK 49 | content: 50 | application/json: 51 | schema: 52 | $ref: '#/components/schemas/EnvironmentResponse' 53 | example: | 54 | { 55 | "os": { 56 | "platform": "linux", 57 | }, 58 | "python": { 59 | "version": "3.6.9 (default, Jun 9 2020, 15:31:15) \n[GCC 7.5.0]", 60 | "executable": "/usr/bin/python3", 61 | "pythonpath": [ 62 | "/usr/lib/python3", 63 | ], 64 | "version_info": { 65 | "major": 3, 66 | "minor": 6, 67 | "micro": 9, 68 | "releaselevel": "final", 69 | "serial": 0 70 | } 71 | }, 72 | "process": { 73 | "argv": [ 74 | "/home/scripts/run-server.py" 75 | ], 76 | "cwd": "/home/scripts", 77 | "user": "scripts", 78 | "pid": 24650, 79 | "environ": { 80 | "TERM": "tmux-256color", 81 | "SHELL": "/bin/bash", 82 | } 83 | }, 84 | } 85 | components: 86 | schemas: 87 | HealthResponse: 88 | description: | 89 | A system healthcheck response as defined originally by Runscope. 90 | Not entirely compatible with MicroProfile Healthcheck protocol definition. 91 | type: object 92 | properties: 93 | hostname: 94 | description: Name of host server. 95 | type: string 96 | status: 97 | description: Verbal description of status. 98 | type: string 99 | pattern: ^(success|failure)$ 100 | example: success 101 | timestamp: 102 | description: A timestamp. 103 | type: string 104 | results: 105 | description: List of Result objects. Should contain at least one. 106 | type: array 107 | items: 108 | $ref: '#/components/schemas/HealthResults' 109 | required: 110 | - status 111 | - results 112 | example: 113 | { 114 | "hostname": "localhost", 115 | "status": "success", 116 | "timestamp": 1598022539.1783307, 117 | "results": [ 118 | { 119 | "checker": "check_is_up", 120 | "output": "UP", 121 | "passed": true, 122 | "timestamp": 1598022539.1783178, 123 | "expires": 1598022566.1783178 124 | } 125 | ] 126 | } 127 | HealthResults: 128 | description: | 129 | The result of an individual health check. 130 | type: object 131 | properties: 132 | checker: 133 | description: Name of checker (name of function registered as a check). 134 | type: string 135 | example: is_up 136 | output: 137 | description: Verbal description of status. 138 | type: string 139 | example: UP 140 | passed: 141 | description: Boolean value to tell if check passed. 142 | type: boolean 143 | example: true 144 | timestamp: 145 | description: A timestamp. 146 | type: string 147 | expires: 148 | description: | 149 | A timestamp to tell when the check will expire. 150 | This can be used to cache results. 151 | type: string 152 | required: 153 | - check 154 | - passed 155 | example: | 156 | { 157 | "checker": "check_is_up", 158 | "output": "UP", 159 | "passed": true, 160 | "timestamp": 1598022539.1783178, 161 | "expires": 1598022566.1783178 162 | } 163 | EnvironmentResponse: 164 | description: | 165 | A description of the system. 166 | type: object 167 | properties: 168 | os: 169 | description: Details about OS. 170 | type: object 171 | python: 172 | description: Details about the current Python executable. 173 | type: object 174 | process: 175 | description: Details about the current process. 176 | type: object 177 | example: 178 | { 179 | "os": { 180 | "platform": "linux", 181 | }, 182 | "python": { 183 | "version": "3.6.9 (default, Jun 9 2020, 15:31:15) \n[GCC 7.5.0]", 184 | "executable": "/usr/bin/python3", 185 | "pythonpath": [ 186 | "/usr/lib/python3", 187 | ], 188 | "version_info": { 189 | "major": 3, 190 | "minor": 6, 191 | "micro": 9, 192 | "releaselevel": "final", 193 | "serial": 0 194 | } 195 | }, 196 | "process": { 197 | "argv": [ 198 | "/home/scripts/run-server.py" 199 | ], 200 | "cwd": "/home/scripts", 201 | "user": "scripts", 202 | "pid": 24650, 203 | "environ": { 204 | "TERM": "tmux-256color", 205 | "SHELL": "/bin/bash", 206 | } 207 | }, 208 | } 209 | # vim: set shiftwidth=2:softtabstop=2:tabstop=2:expandtab:autoindent : 210 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Healthcheck 2 | ----------- 3 | 4 | .. image:: https://github.com/ateliedocodigo/py-healthcheck/raw/develop/py-healthcheck.jpg 5 | :target: https://pypi.python.org/pypi/py-healthcheck 6 | 7 | .. image:: https://badge.fury.io/py/py-healthcheck.svg 8 | :target: https://badge.fury.io/py/py-healthcheck 9 | 10 | .. image:: https://requires.io/github/ateliedocodigo/py-healthcheck/requirements.svg?branch=develop 11 | :target: https://requires.io/github/ateliedocodigo/py-healthcheck/requirements/?branch=develop 12 | :alt: Requirements Status 13 | 14 | .. image:: https://travis-ci.org/ateliedocodigo/py-healthcheck.svg?branch=develop 15 | :target: https://travis-ci.org/ateliedocodigo/py-healthcheck 16 | 17 | 18 | Healthcheck is a library to write simple healthcheck functions that can 19 | be used to monitor your application. It is possible to use in a ``Flask`` 20 | app or ``Tornado`` app. It's useful for asserting that your dependencies 21 | are up and running and your application can respond to HTTP requests. 22 | The Healthcheck functions can be exposed via a user defined ``Flask`` 23 | route so you can use an external monitoring application (``monit``, 24 | ``nagios``, ``Runscope``, etc.) to check the status and uptime of your 25 | application. 26 | 27 | New in version 1.1: Healthcheck also gives you a simple Flask route to 28 | view information about your application's environment. By default, this 29 | includes data about the operating system, the Python environment, the 30 | current process, and the application config. You can customize which 31 | sections are included, or add your own sections to the output. 32 | 33 | Installing 34 | ---------- 35 | 36 | :: 37 | 38 | pip install py-healthcheck 39 | 40 | Usage 41 | ----- 42 | 43 | Here's an example of basic usage with ``Flask``: 44 | 45 | .. code:: python 46 | 47 | from flask import Flask 48 | from healthcheck import HealthCheck, EnvironmentDump 49 | 50 | app = Flask(__name__) 51 | 52 | health = HealthCheck() 53 | envdump = EnvironmentDump() 54 | 55 | # add your own check function to the healthcheck 56 | def redis_available(): 57 | client = _redis_client() 58 | info = client.info() 59 | return True, "redis ok" 60 | 61 | health.add_check(redis_available) 62 | 63 | # add your own data to the environment dump 64 | def application_data(): 65 | return {"maintainer": "Luis Fernando Gomes", 66 | "git_repo": "https://github.com/ateliedocodigo/py-healthcheck"} 67 | 68 | envdump.add_section("application", application_data) 69 | 70 | # Add a flask route to expose information 71 | app.add_url_rule("/healthcheck", "healthcheck", view_func=health.run) 72 | app.add_url_rule("/environment", "environment", view_func=envdump.run) 73 | 74 | To use with ``Tornado`` you can import the ``TornadoHandler``: 75 | 76 | .. code:: python 77 | 78 | import tornado.web 79 | from healthcheck import TornadoHandler, HealthCheck, EnvironmentDump 80 | 81 | app = tornado.web.Application() 82 | 83 | health = HealthCheck() 84 | envdump = EnvironmentDump() 85 | 86 | # add your own check function to the healthcheck 87 | def redis_available(): 88 | client = _redis_client() 89 | info = client.info() 90 | return True, "redis ok" 91 | 92 | health.add_check(redis_available) 93 | 94 | # add your own data to the environment dump or healthcheck 95 | def application_data(): 96 | return {"maintainer": "Luis Fernando Gomes", 97 | "git_repo": "https://github.com/ateliedocodigo/py-healthcheck"} 98 | 99 | # ou choose where you want to output this information 100 | health.add_section("application", application_data) 101 | health.add_section("version", __version__) 102 | envdump.add_section("application", application_data) 103 | 104 | # Add a tornado handler to expose information 105 | app.add_handlers( 106 | r".*", 107 | [ 108 | ( 109 | "/healthcheck", 110 | TornadoHandler, dict(checker=health) 111 | ), 112 | ( 113 | "/environment", 114 | TornadoHandler, dict(checker=envdump) 115 | ), 116 | ] 117 | ) 118 | 119 | Alternatively you can set all together: 120 | 121 | .. code:: python 122 | 123 | import tornado.web 124 | from healthcheck import TornadoHandler, HealthCheck, EnvironmentDump 125 | 126 | # add your own check function to the healthcheck 127 | def redis_available(): 128 | client = _redis_client() 129 | info = client.info() 130 | return True, "redis ok" 131 | 132 | health = HealthCheck(checkers=[redis_available]) 133 | 134 | # add your own data to the environment dump 135 | def application_data(): 136 | return {"maintainer": "Luis Fernando Gomes", 137 | "git_repo": "https://github.com/ateliedocodigo/py-healthcheck"} 138 | 139 | envdump = EnvironmentDump(application=application_data) 140 | 141 | app = tornado.web.Application([ 142 | ("/healthcheck", TornadoHandler, dict(checker=health)), 143 | ("/environment", TornadoHandler, dict(checker=envdump)), 144 | ]) 145 | 146 | To run all of your check functions, make a request to the healthcheck 147 | URL you specified, like this: 148 | 149 | :: 150 | 151 | curl "http://localhost:5000/healthcheck" 152 | 153 | And to view the environment data, make a check to the URL you specified 154 | for EnvironmentDump: 155 | 156 | :: 157 | 158 | curl "http://localhost:5000/environment" 159 | 160 | The HealthCheck class 161 | --------------------- 162 | 163 | Check Functions 164 | ~~~~~~~~~~~~~~~ 165 | 166 | Check functions take no arguments and should return a tuple of (bool, 167 | str). The boolean is whether or not the check passed. The message is any 168 | string or output that should be rendered for this check. Useful for 169 | error messages/debugging. 170 | 171 | .. code:: python 172 | 173 | # add check functions 174 | def addition_works(): 175 | if 1 + 1 == 2: 176 | return True, "addition works" 177 | else: 178 | return False, "the universe is broken" 179 | 180 | Any exceptions that get thrown by your code will be caught and handled 181 | as errors in the healthcheck: 182 | 183 | .. code:: python 184 | 185 | # add check functions 186 | def throws_exception(): 187 | bad_var = None 188 | bad_var['explode'] 189 | 190 | Will output: 191 | 192 | .. code:: json 193 | 194 | { 195 | "status": "failure", 196 | "results": [ 197 | { 198 | "output": "'NoneType' object has no attribute '__getitem__'", 199 | "checker": "throws_exception", 200 | "passed": false 201 | } 202 | ] 203 | } 204 | 205 | Note, all checkers will get run and all failures will be reported. It's 206 | intended that they are all separate checks and if any one fails the 207 | healthcheck overall is failed. 208 | 209 | Caching 210 | ~~~~~~~ 211 | 212 | In Runscope's infrastructure, the /healthcheck endpoint is hit 213 | surprisingly often. haproxy runs on every server, and each haproxy hits 214 | every healthcheck twice a minute. (So if we have 30 servers in our 215 | infrastructure, that's 60 healthchecks per minute to every Flask 216 | service.) Plus, monit hits every healthcheck 6 times a minute. 217 | 218 | To avoid putting too much strain on backend services, health check 219 | results can be cached in process memory. By default, health checks that 220 | succeed are cached for 27 seconds, and failures are cached for 9 221 | seconds. These can be overridden with the ``success_ttl`` and 222 | ``failed_ttl`` parameters. If you don't want to use the cache at all, 223 | initialize the Healthcheck object with 224 | ``success_ttl=None, failed_ttl=None``. 225 | 226 | Customizing 227 | ~~~~~~~~~~~ 228 | 229 | You can customize the status codes, headers, and output format for 230 | success and failure responses. 231 | 232 | The EnvironmentDump class 233 | ------------------------- 234 | 235 | Built-in data sections 236 | ~~~~~~~~~~~~~~~~~~~~~~ 237 | 238 | By default, EnvironmentDump data includes these 4 sections: 239 | 240 | - ``os``: information about your operating system. 241 | - ``python``: information about your Python executable, Python path, 242 | and installed packages. 243 | - ``process``: information about the currently running Python process, 244 | including the PID, command line arguments, and all environment 245 | variables. 246 | 247 | Some of the data is scrubbed to avoid accidentally exposing passwords or 248 | access keys/tokens. Config keys and environment variable names are 249 | scanned for ``key``, ``token``, or ``pass``. If those strings are 250 | present in the name of the variable, the value is not included. 251 | 252 | Disabling built-in data sections 253 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 254 | 255 | For security reasons, you may want to disable an entire section. You can 256 | disable sections when you instantiate the ``EnvironmentDump`` object, 257 | like this: 258 | 259 | .. code:: python 260 | 261 | envdump = EnvironmentDump(include_python=False, 262 | include_os=False, 263 | include_process=False) 264 | 265 | Adding custom data sections 266 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 267 | 268 | You can add a new section to the output by registering a function of 269 | your own. Here's an example of how this would be used: 270 | 271 | .. code:: python 272 | 273 | def application_data(): 274 | return {"maintainer": "Luis Fernando Gomes", 275 | "git_repo": "https://github.com/ateliedocodigo/py-healthcheck" 276 | "config": app.config} 277 | 278 | envdump = EnvironmentDump() 279 | envdump.add_section("application", application_data) 280 | 281 | 282 | Credits 283 | ------- 284 | 285 | This project was forked from `Runscope/healthcheck 286 | `_. since ``1.3.1`` 287 | --------------------------------------------------------------------------------