|
| 1 | +import asyncio |
| 2 | +import imp |
| 3 | +import json |
| 4 | +import os |
| 5 | +import logging |
| 6 | +import socket |
| 7 | +import sys |
| 8 | +import time |
| 9 | +import traceback |
| 10 | +from aiohttp import web |
| 11 | + |
| 12 | + |
| 13 | +try: |
| 14 | + from functools import reduce |
| 15 | +except Exception: |
| 16 | + pass |
| 17 | + |
| 18 | + |
| 19 | +def basic_exception_handler(_, e): |
| 20 | + return False, str(e) |
| 21 | + |
| 22 | + |
| 23 | +def json_success_handler(results): |
| 24 | + data = { |
| 25 | + 'hostname': socket.gethostname(), |
| 26 | + 'status': 'success', |
| 27 | + 'timestamp': time.time(), |
| 28 | + 'results': results, |
| 29 | + } |
| 30 | + |
| 31 | + return json.dumps(data) |
| 32 | + |
| 33 | + |
| 34 | +def json_failed_handler(results): |
| 35 | + data = { |
| 36 | + 'hostname': socket.gethostname(), |
| 37 | + 'status': 'failure', |
| 38 | + 'timestamp': time.time(), |
| 39 | + 'results': results, |
| 40 | + } |
| 41 | + |
| 42 | + return json.dumps(data) |
| 43 | + |
| 44 | + |
| 45 | +def check_reduce(passed, result): |
| 46 | + return passed and result.get('passed') |
| 47 | + |
| 48 | + |
| 49 | +class Check(object): |
| 50 | + def __init__(self, success_status=200, success_headers=None, |
| 51 | + success_handler=json_success_handler, success_ttl=None, |
| 52 | + failed_status=500, failed_headers=None, |
| 53 | + failed_handler=json_failed_handler, failed_ttl=None, |
| 54 | + exception_handler=basic_exception_handler, checkers=None, |
| 55 | + logger=None, **options): |
| 56 | + self.cache = dict() |
| 57 | + |
| 58 | + self.success_status = success_status |
| 59 | + self.success_headers = success_headers or {'Content-Type': 'application/json'} |
| 60 | + self.success_handler = success_handler |
| 61 | + self.success_ttl = float(success_ttl or 0) |
| 62 | + |
| 63 | + self.failed_status = failed_status |
| 64 | + self.failed_headers = failed_headers or {'Content-Type': 'application/json'} |
| 65 | + self.failed_handler = failed_handler |
| 66 | + self.failed_ttl = float(failed_ttl or 0) |
| 67 | + |
| 68 | + self.exception_handler = exception_handler |
| 69 | + |
| 70 | + self.options = options |
| 71 | + self.checkers = checkers or [] |
| 72 | + |
| 73 | + self.logger = logger |
| 74 | + if not self.logger: |
| 75 | + self.logger = logging.getLogger('HealthCheck') |
| 76 | + |
| 77 | + @asyncio.coroutine |
| 78 | + def __call__(self, request): |
| 79 | + message, status, headers = yield from self.check() |
| 80 | + return web.Response(text=message, status=status, headers=headers) |
| 81 | + |
| 82 | + def add_check(self, func): |
| 83 | + if not asyncio.iscoroutinefunction(func): |
| 84 | + func = asyncio.coroutine(func) |
| 85 | + |
| 86 | + self.checkers.append(func) |
| 87 | + |
| 88 | + @asyncio.coroutine |
| 89 | + def run_check(self, checker): |
| 90 | + try: |
| 91 | + passed, output = yield from checker() |
| 92 | + except Exception: |
| 93 | + traceback.print_exc() |
| 94 | + e = sys.exc_info()[0] |
| 95 | + self.logger.exception(e) |
| 96 | + passed, output = self.exception_handler(checker, e) |
| 97 | + |
| 98 | + if not passed: |
| 99 | + msg = 'Health check "{}" failed with output "{}"'.format(checker.__name__, output) |
| 100 | + self.logger.error(msg) |
| 101 | + |
| 102 | + timestamp = time.time() |
| 103 | + if passed: |
| 104 | + expires = timestamp + self.success_ttl |
| 105 | + else: |
| 106 | + expires = timestamp + self.failed_ttl |
| 107 | + |
| 108 | + result = {'checker': checker.__name__, |
| 109 | + 'output': output, |
| 110 | + 'passed': passed, |
| 111 | + 'timestamp': timestamp, |
| 112 | + 'expires': expires} |
| 113 | + return result |
| 114 | + |
| 115 | + @asyncio.coroutine |
| 116 | + def check(self): |
| 117 | + results = [] |
| 118 | + for checker in self.checkers: |
| 119 | + if checker in self.cache and self.cache[checker].get('expires') >= time.time(): |
| 120 | + result = self.cache[checker] |
| 121 | + else: |
| 122 | + result = yield from self.run_check(checker) |
| 123 | + self.cache[checker] = result |
| 124 | + results.append(result) |
| 125 | + |
| 126 | + passed = reduce(check_reduce, results, True) |
| 127 | + |
| 128 | + if passed: |
| 129 | + message = "OK" |
| 130 | + if self.success_handler: |
| 131 | + message = self.success_handler(results) |
| 132 | + |
| 133 | + return message, self.success_status, self.success_headers |
| 134 | + else: |
| 135 | + message = "NOT OK" |
| 136 | + if self.failed_handler: |
| 137 | + message = self.failed_handler(results) |
| 138 | + |
| 139 | + return message, self.failed_status, self.failed_headers |
| 140 | + |
| 141 | + |
| 142 | +class EnvDump(object): |
| 143 | + def __init__(self, |
| 144 | + include_os=False, |
| 145 | + include_python=False, |
| 146 | + include_process=False): |
| 147 | + |
| 148 | + self.functions = {} |
| 149 | + |
| 150 | + if include_os: |
| 151 | + self.functions['os'] = self.get_os |
| 152 | + if include_python: |
| 153 | + self.functions['python'] = self.get_python |
| 154 | + if include_process: |
| 155 | + self.functions['process'] = self.get_process |
| 156 | + |
| 157 | + @asyncio.coroutine |
| 158 | + def __call__(self, request): |
| 159 | + data = yield from self.dump_environment(request) |
| 160 | + return web.json_response(data) |
| 161 | + |
| 162 | + @asyncio.coroutine |
| 163 | + def dump_environment(self, request): |
| 164 | + data = {} |
| 165 | + data['storage'] = yield from self.get_storage_info(request) |
| 166 | + |
| 167 | + for name, func in self.functions.items(): |
| 168 | + data[name] = yield from func() |
| 169 | + |
| 170 | + return data |
| 171 | + |
| 172 | + @asyncio.coroutine |
| 173 | + def get_os(self): |
| 174 | + return {'platform': sys.platform, |
| 175 | + 'name': os.name, |
| 176 | + 'uname': os.uname()} |
| 177 | + |
| 178 | + @asyncio.coroutine |
| 179 | + def get_python(self): |
| 180 | + result = {'version': sys.version, |
| 181 | + 'executable': sys.executable, |
| 182 | + 'pythonpath': sys.path, |
| 183 | + 'version_info': {'major': sys.version_info.major, |
| 184 | + 'minor': sys.version_info.minor, |
| 185 | + 'micro': sys.version_info.micro, |
| 186 | + 'releaselevel': sys.version_info.releaselevel, |
| 187 | + 'serial': sys.version_info.serial}} |
| 188 | + if imp.find_module('pkg_resources'): |
| 189 | + import pkg_resources |
| 190 | + packages = dict([(p.project_name, p.version) for p in pkg_resources.working_set]) |
| 191 | + result['packages'] = packages |
| 192 | + |
| 193 | + return result |
| 194 | + |
| 195 | + @asyncio.coroutine |
| 196 | + def get_login(self): |
| 197 | + # Based on https://github.com/gitpython-developers/GitPython/pull/43/ |
| 198 | + # Fix for 'Inappropriate ioctl for device' on posix systems. |
| 199 | + if os.name == "posix": |
| 200 | + import pwd |
| 201 | + username = pwd.getpwuid(os.geteuid()).pw_name |
| 202 | + else: |
| 203 | + username = os.environ.get('USER', os.environ.get('USERNAME', 'UNKNOWN')) |
| 204 | + if username == 'UNKNOWN' and hasattr(os, 'getlogin'): |
| 205 | + username = os.getlogin() |
| 206 | + return username |
| 207 | + |
| 208 | + @asyncio.coroutine |
| 209 | + def get_process(self): |
| 210 | + return {'argv': sys.argv, |
| 211 | + 'cwd': os.getcwd(), |
| 212 | + 'user': (yield from self.get_login()), |
| 213 | + 'pid': os.getpid(), |
| 214 | + 'environ': self.safe_dump(os.environ)} |
| 215 | + |
| 216 | + @asyncio.coroutine |
| 217 | + def get_storage_info(self, request): |
| 218 | + storage_path = request.app["settings"]["storage_path"] |
| 219 | + dir_count = 0 |
| 220 | + file_count = 0 |
| 221 | + total = 0 |
| 222 | + with os.scandir(storage_path) as it: |
| 223 | + for entry in it: |
| 224 | + if entry.is_file(): |
| 225 | + file_count += 1 |
| 226 | + total += entry.stat().st_size |
| 227 | + elif entry.is_dir(): |
| 228 | + dir_count += 1 |
| 229 | + total += get_dir_size(entry.path) |
| 230 | + |
| 231 | + return {'number_of_files': file_count, |
| 232 | + 'number_of_directories': dir_count, |
| 233 | + 'used_space': total} |
| 234 | + |
| 235 | + @staticmethod |
| 236 | + def safe_dump(dictionary): |
| 237 | + result = {} |
| 238 | + for key in dictionary.keys(): |
| 239 | + if 'key' in key.lower() or 'token' in key.lower() or 'pass' in key.lower(): |
| 240 | + # Try to avoid listing passwords and access tokens or keys in the output |
| 241 | + result[key] = "********" |
| 242 | + else: |
| 243 | + try: |
| 244 | + json.dumps(dictionary[key]) |
| 245 | + result[key] = dictionary[key] |
| 246 | + except TypeError: |
| 247 | + pass |
| 248 | + return result |
0 commit comments