├── .gitignore ├── README.md ├── hchk ├── __init__.py └── cli.py ├── setup.cfg ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | dist/ 4 | build/ 5 | *.egg-info/ 6 | 7 | .tox/ 8 | .coverage 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # healthchecks.io CLI 2 | 3 | [![Not Maintained](https://img.shields.io/badge/Maintenance%20Level-Not%20Maintained-yellow.svg)](https://gist.github.com/cheerfulstoic/d107229326a01ff0f333a1d3476e068d) 4 | 5 | A CLI interface to healthchecks.io. This project **is not maintained**, and I currently 6 | have no plans to develop it further. For alternative, better, and actively maintained 7 | CLI tools, please see [Third-Party Resources](https://healthchecks.io/docs/resources/) 8 | on Healthchecks.io. 9 | 10 | # Installation 11 | 12 | Run: 13 | 14 | $ pip install hchk 15 | 16 | 17 | # Usage 18 | 19 | See available commands: 20 | 21 | $ hchk --help 22 | 23 | Create your healthchecks.io API key in your 24 | [settings page](https://healthchecks.io/accounts/profile/). 25 | 26 | Save the API key on your target system. Your API key will be saved 27 | in a plain text configuration file `$HOME/.hchk` 28 | 29 | $ hchk setkey YOUR_API_KEY 30 | 31 | Create a check with custom name, tags, period and grace time, and then ping 32 | it: 33 | 34 | $ hchk ping -n "My New Check" -t "web prod" -p 600 -g 60 35 | 36 | The check will be created if it does not exist, and its ping URL 37 | will be saved in `$HOME/.hchk`. 38 | 39 | -------------------------------------------------------------------------------- /hchk/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/healthchecks/hchk/3554ca69d8ccec942fcc10f49ef627f562d47bff/hchk/__init__.py -------------------------------------------------------------------------------- /hchk/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | import json 3 | import os 4 | import pkg_resources 5 | import requests 6 | import sys 7 | import time 8 | 9 | try: 10 | # Python 3 11 | from configparser import RawConfigParser 12 | except ImportError: 13 | # Python 2 14 | from ConfigParser import RawConfigParser 15 | 16 | CHECK_ARGS = ("name", "tags", "period", "grace") 17 | INI_PATH = os.path.join(os.path.expanduser("~"), ".hchk") 18 | VERSION = pkg_resources.get_distribution("hchk").version 19 | UA = "hchk/%s" % VERSION 20 | USE_SSL = True 21 | if sys.version_info[0:3] < (2, 7, 9): 22 | # Certificate validation is a mess on Python < 2.7.9 23 | USE_SSL = False 24 | 25 | 26 | class Api(object): 27 | def __init__(self, api_key): 28 | self.api_key = api_key 29 | 30 | def create_check(self, check): 31 | payload = {"api_key": self.api_key} 32 | if check.get("name"): 33 | payload["name"] = check["name"] 34 | if check.get("tags"): 35 | payload["tags"] = check["tags"] 36 | if check.get("period"): 37 | payload["timeout"] = int(check["period"]) 38 | if check.get("grace"): 39 | payload["grace"] = int(check["grace"]) 40 | 41 | url = "https://healthchecks.io/api/v1/checks/" 42 | data = json.dumps(payload) 43 | r = requests.post(url, data=data, headers={"User-Agent": UA}, 44 | verify=USE_SSL) 45 | parsed = r.json() 46 | if "error" in r: 47 | raise ValueError(r["error"]) 48 | 49 | return parsed["ping_url"] 50 | 51 | 52 | class Check(dict): 53 | def matches_spec(self, spec): 54 | for key in CHECK_ARGS: 55 | if self.get(key) != spec.get(key): 56 | return False 57 | 58 | return True 59 | 60 | def create(self, api): 61 | self["ping_url"] = api.create_check(self) 62 | 63 | def ping(self): 64 | """Run HTTP GET to self["ping_url"]. 65 | 66 | On errors, retry with exponential backoff. 67 | On Python version below 2.7.9 fall back from https to http. 68 | 69 | """ 70 | 71 | url = self["ping_url"] 72 | if url.startswith("https://") and not USE_SSL: 73 | url = url.replace("https://", "http://") 74 | 75 | retries = 0 76 | while True: 77 | status = 0 78 | try: 79 | r = requests.get(url, timeout=10, headers={"User-Agent": UA}) 80 | status = r.status_code 81 | except requests.exceptions.ConnectionError: 82 | sys.stderr.write("Connection error\n") 83 | except requests.exceptions.Timeout: 84 | sys.stderr.write("Connection timed out\n") 85 | else: 86 | if status not in (200, 400): 87 | sys.stderr.write("Received HTTP status %d\n" % status) 88 | 89 | # 200 is success, 400 is most likely "check does not exist" 90 | if status in (200, 400): 91 | return status 92 | 93 | # In any other case, let's retry with exponential backoff: 94 | delay, retries = 2 ** retries, retries + 1 95 | if retries >= 5: 96 | sys.stderr.write("Exceeded max retries, giving up\n") 97 | return 0 98 | 99 | sys.stderr.write("Will retry after %ds\n" % delay) 100 | time.sleep(delay) 101 | 102 | 103 | class Config(RawConfigParser): 104 | def __init__(self): 105 | # RawConfigParser is old-style class, so don't use super() 106 | RawConfigParser.__init__(self) 107 | self.read(INI_PATH) 108 | 109 | def save(self): 110 | with open(INI_PATH, 'w') as f: 111 | self.write(f) 112 | 113 | def find(self, spec): 114 | for section in self.sections(): 115 | if not self.has_option(section, "ping_url"): 116 | continue 117 | 118 | candidate = Check(self.items(section)) 119 | if candidate.matches_spec(spec): 120 | candidate["_section"] = section 121 | return candidate 122 | 123 | def save_check(self, check): 124 | # First, remove all checks with similar specs 125 | while True: 126 | other = self.find(check) 127 | if other is None: 128 | break 129 | 130 | self.remove_section(other["_section"]) 131 | 132 | # Then save this check 133 | code = check["ping_url"].split("/")[-1] 134 | self.add_section(code) 135 | self.set(code, "ping_url", check["ping_url"]) 136 | for key in CHECK_ARGS: 137 | if check.get(key): 138 | self.set(code, key, check[key]) 139 | 140 | self.save() 141 | 142 | def get_api_key(self): 143 | if not self.has_option("hchk", "api_key"): 144 | return None 145 | 146 | return self.get("hchk", "api_key") 147 | 148 | 149 | @click.group() 150 | def cli(): 151 | """A CLI interface to healthchecks.io""" 152 | 153 | pass 154 | 155 | 156 | @cli.command() 157 | @click.option('--name', '-n', help='Name for the new check') 158 | @click.option('--tags', '-t', 159 | help='Space-delimited list of tags for the new check') 160 | @click.option('--period', '-p', help='Period, a number of seconds') 161 | @click.option('--grace', '-g', help='Grace time, a number of seconds') 162 | def ping(**kwargs): 163 | """Create a check if neccessary, then ping it.""" 164 | 165 | config = Config() 166 | api_key = config.get_api_key() 167 | if api_key is None: 168 | msg = """API key is not set. Please set it with 169 | 170 | hchk setkey YOUR_API_KEY 171 | 172 | """ 173 | sys.stderr.write(msg) 174 | sys.exit(1) 175 | 176 | api = Api(api_key) 177 | 178 | spec = {} 179 | for key in CHECK_ARGS: 180 | if kwargs.get(key): 181 | spec[key] = kwargs[key] 182 | 183 | check = config.find(spec) 184 | if check is None: 185 | check = Check(spec) 186 | check.create(api) 187 | config.save_check(check) 188 | 189 | status = check.ping() 190 | if status == 400: 191 | check.create(api) 192 | config.save_check(check) 193 | status = check.ping() 194 | 195 | if status != 200: 196 | sys.exit(1) 197 | 198 | 199 | @cli.command() 200 | @click.argument('api_key') 201 | def setkey(api_key): 202 | """Save API key in $HOME/.hchk""" 203 | 204 | config = Config() 205 | if not config.has_section("hchk"): 206 | config.add_section("hchk") 207 | 208 | config.set("hchk", "api_key", api_key) 209 | config.save() 210 | 211 | print("API key saved!") 212 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | A CLI interface to healthchecks.io 5 | """ 6 | from setuptools import find_packages, setup 7 | 8 | dependencies = ['click', 'requests'] 9 | 10 | setup( 11 | name='hchk', 12 | version='0.1.4', 13 | url='https://github.com/healthchecks/hchk', 14 | license='BSD', 15 | author='Pēteris Caune', 16 | author_email='cuu508@gmail.com', 17 | description='A CLI interface to healthchecks.io', 18 | long_description=__doc__, 19 | packages=find_packages(exclude=['tests']), 20 | include_package_data=True, 21 | zip_safe=False, 22 | platforms='any', 23 | install_requires=dependencies, 24 | entry_points={ 25 | 'console_scripts': [ 26 | 'hchk = hchk.cli:cli', 27 | ], 28 | }, 29 | classifiers=[ 30 | # As from http://pypi.python.org/pypi?%3Aaction=list_classifiers 31 | # 'Development Status :: 1 - Planning', 32 | # 'Development Status :: 2 - Pre-Alpha', 33 | # 'Development Status :: 3 - Alpha', 34 | 'Development Status :: 4 - Beta', 35 | # 'Development Status :: 5 - Production/Stable', 36 | # 'Development Status :: 6 - Mature', 37 | # 'Development Status :: 7 - Inactive', 38 | 'Environment :: Console', 39 | 'Intended Audience :: Developers', 40 | 'License :: OSI Approved :: BSD License', 41 | 'Operating System :: POSIX', 42 | 'Operating System :: MacOS', 43 | 'Operating System :: Unix', 44 | 'Programming Language :: Python', 45 | 'Programming Language :: Python :: 2', 46 | 'Programming Language :: Python :: 3', 47 | 'Topic :: Software Development :: Libraries :: Python Modules', 48 | ] 49 | ) 50 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py26, py27, py33, py34, pypy, flake8 3 | 4 | [testenv] 5 | commands=py.test --cov hchk {posargs} 6 | deps= 7 | pytest 8 | pytest-cov 9 | 10 | [testenv:flake8] 11 | basepython = python2.7 12 | deps = 13 | flake8 14 | commands = 15 | flake8 hchk tests --max-line-length=120 16 | --------------------------------------------------------------------------------