From 4ae63e1b4de1663ebe1aa9229b919ddcb58e77ba Mon Sep 17 00:00:00 2001 From: James Brown Date: Fri, 2 Oct 2015 17:53:15 -0700 Subject: [PATCH 1/6] support syslog as a logging method --- README.md | 15 +++++++++------ hacheck/main.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 101ee33..5feaa1f 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,15 @@ Imagine you want to take down the server `web01` for maintenance. Just SSH to it ### Configuration `hacheck` accepts a `-c` flag which should point to a YAML-formatted configuration file. Some notable properties of this file: -* `cache_time`: The duration for which check results may be cached -* `service_name_header`: If set, the name of a header which will be populated with the service name on HTTP checks -* `log_path`: Either the string `"stdout"`, the string `"stderr"`, or a fully-qualified path to a file to write logs to. Uses a [WatchedFileHandler](http://docs.python.org/2/library/logging.handlers.html#watchedfilehandler) and ought to play nicely with logrotate -* `mysql_username`: username to use when logging into mysql for checks -* `mysql_password`: password to use when logging into mysql for checks -* `rlimit_nofile`: set the NOFILE rlimit. If the string "max", will set the rlimit to the hard rlimit; otherwise, will be interpreted as an integer and set to that value. + + * `cache_time`: The duration for which check results may be cached + * `service_name_header`: If set, the name of a header which will be populated with the service name on HTTP checks + * `log_path`: Either the string `"stdout"`, the string `"stderr"`, the string "syslog", or a fully-qualified path to a file to write logs to. + * If passed `syslog`, will write datagram messages to `/dev/log`. Can be passed `syslog,address,socktype` to write to other kinds of syslog. For example, `syslog,127.0.0.1:514,dgram` will write UDP to localhost:514; `syslog,/dev/log_stream,stream` will write stream (TCP) to `/dev/log` + * Files use a [WatchedFileHandler](http://docs.python.org/2/library/logging.handlers.html#watchedfilehandler) and ought to play nicely with logrotate + * `mysql_username`: username to use when logging into mysql for checks + * `mysql_password`: password to use when logging into mysql for checks + * `rlimit_nofile`: set the NOFILE rlimit. If the string "max", will set the rlimit to the hard rlimit; otherwise, will be interpreted as an integer and set to that value. ### Monitoring diff --git a/hacheck/main.py b/hacheck/main.py index 6fd7343..bf1e8d6 100644 --- a/hacheck/main.py +++ b/hacheck/main.py @@ -3,6 +3,7 @@ import signal import time import sys +import socket import resource import tornado.ioloop @@ -42,7 +43,7 @@ def get_app(): (r'/recent', handlers.ListRecentHandler), (r'/status/count', handlers.ServiceCountHandler), (r'/status', handlers.StatusHandler), - ], start_time=time.time(), log_function=log_request) + ], start_time=time.time(), log_function=log_request) def setrlimit_nofile(soft_target): @@ -105,6 +106,33 @@ def main(): handler = logging.StreamHandler(sys.stdout) elif log_path == 'stderr': handler = logging.StreamHandler(sys.stderr) + elif log_path.startswith('syslog'): + import syslog + if log_path == 'syslog': + address = '/dev/log' + else: + syslog_address_parts = log_path.split(',') + if len(syslog_address_parts) == 3: + _, syslog_address, socktype = syslog_address_parts + socktypes = { + 'stream': socket.SOCK_STREAM, + 'dgram': socket.SOCK_DGRAM + } + if socktype not in socktypes: + raise ValueError('Unrecognized socket type {0}'.format(socktype)) + socktype = socktypes[socktype] + elif len(syslog_address_parts) == 2: + _, syslog_address = syslog_address_parts + socktype = socket.SOCK_DGRAM + else: + raise ValueError('Unrecognized syslog address format {0}'.format(log_path)) + if ':' in syslog_address: + host, port = syslog_address.split(':') + port = int(port) + address = (host, port) + else: + address = syslog_address + handler = logging.handlers.SysLogHandler(address, facility=syslog.LOG_DAEMON, socktype=socktype) else: handler = logging.handlers.WatchedFileHandler(log_path) fmt = logging.Formatter(logging.BASIC_FORMAT, None) From 07bc3f5a37d68615ccd8c4cb274f115e7c59f6ef Mon Sep 17 00:00:00 2001 From: James Brown Date: Mon, 5 Oct 2015 18:04:13 -0700 Subject: [PATCH 2/6] initialize socktype --- hacheck/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hacheck/main.py b/hacheck/main.py index bf1e8d6..4fc5139 100644 --- a/hacheck/main.py +++ b/hacheck/main.py @@ -108,6 +108,7 @@ def main(): handler = logging.StreamHandler(sys.stderr) elif log_path.startswith('syslog'): import syslog + socktype = socket.SOCK_DGRAM if log_path == 'syslog': address = '/dev/log' else: @@ -123,7 +124,6 @@ def main(): socktype = socktypes[socktype] elif len(syslog_address_parts) == 2: _, syslog_address = syslog_address_parts - socktype = socket.SOCK_DGRAM else: raise ValueError('Unrecognized syslog address format {0}'.format(log_path)) if ':' in syslog_address: From 4951ec72980c08696d1aac402c169291bc7e0ecc Mon Sep 17 00:00:00 2001 From: James Brown Date: Mon, 5 Oct 2015 18:18:41 -0700 Subject: [PATCH 3/6] allow interpolating environment variables in config properties --- README.md | 2 ++ hacheck/config.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5feaa1f..d419ae1 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ Imagine you want to take down the server `web01` for maintenance. Just SSH to it * `mysql_password`: password to use when logging into mysql for checks * `rlimit_nofile`: set the NOFILE rlimit. If the string "max", will set the rlimit to the hard rlimit; otherwise, will be interpreted as an integer and set to that value. +The values in this configuration may contain shell variables, written as ${VARNAME}. These will be expanded at runtime. + ### Monitoring `hacheck` exports some useful monitoring stuff at the `/status` endpoint. It also exports a count of requests by source-IP and service name on the `/status/count` endpoint. diff --git a/hacheck/config.py b/hacheck/config.py index 58e869b..df120f2 100644 --- a/hacheck/config.py +++ b/hacheck/config.py @@ -1,4 +1,6 @@ import yaml +import os +import re def max_or_int(some_str_value): @@ -23,11 +25,22 @@ def max_or_int(some_str_value): config[key] = default +def expand_shell_variables(key, value): + def get_env(vmd): + variable = vmd.group(1) + if variable in os.environ: + return os.environ[variable] + else: + raise KeyError('${0} not in environment (from config {1}={2})'.format(variable, key, value)) + + return re.sub(r'\$\{([^}]+)\}', get_env, value) + + def load_from(path): with open(path, 'r') as f: c = yaml.safe_load(f) for key, value in c.items(): if key in DEFAULTS: constructor, default = DEFAULTS[key] - config[key] = constructor(value) + config[key] = constructor(expand_shell_variables(key, value)) return config From 0cbac0d366184e27691c3947e948dfc15eb80a3d Mon Sep 17 00:00:00 2001 From: James Brown Date: Mon, 5 Oct 2015 18:22:22 -0700 Subject: [PATCH 4/6] fix tests --- hacheck/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hacheck/config.py b/hacheck/config.py index df120f2..258e3af 100644 --- a/hacheck/config.py +++ b/hacheck/config.py @@ -26,6 +26,9 @@ def max_or_int(some_str_value): def expand_shell_variables(key, value): + if not isinstance(value, basestring): + return value + def get_env(vmd): variable = vmd.group(1) if variable in os.environ: From 72124f965f260262a9f6eec2a644f617668a3631 Mon Sep 17 00:00:00 2001 From: James Brown Date: Mon, 5 Oct 2015 18:33:23 -0700 Subject: [PATCH 5/6] fix tests on py3. facepalm --- hacheck/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hacheck/config.py b/hacheck/config.py index 258e3af..6445992 100644 --- a/hacheck/config.py +++ b/hacheck/config.py @@ -2,6 +2,8 @@ import os import re +import six + def max_or_int(some_str_value): if some_str_value == 'max': @@ -26,7 +28,7 @@ def max_or_int(some_str_value): def expand_shell_variables(key, value): - if not isinstance(value, basestring): + if not isinstance(value, six.string_types): return value def get_env(vmd): From 3fb032810e7d0179f66c6eb5fbf01b969b66a48f Mon Sep 17 00:00:00 2001 From: James Brown Date: Thu, 14 Jan 2016 18:57:30 -0800 Subject: [PATCH 6/6] Fix facility definition when logging to syslog The syslog module pre-multiplies the facility value by 8, whereas the SysLogHandler class in the logging.handlers module expects the facility value to match the standard one from --- hacheck/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hacheck/main.py b/hacheck/main.py index 4fc5139..f9c5c7f 100644 --- a/hacheck/main.py +++ b/hacheck/main.py @@ -107,7 +107,6 @@ def main(): elif log_path == 'stderr': handler = logging.StreamHandler(sys.stderr) elif log_path.startswith('syslog'): - import syslog socktype = socket.SOCK_DGRAM if log_path == 'syslog': address = '/dev/log' @@ -132,7 +131,9 @@ def main(): address = (host, port) else: address = syslog_address - handler = logging.handlers.SysLogHandler(address, facility=syslog.LOG_DAEMON, socktype=socktype) + handler = logging.handlers.SysLogHandler( + address, facility=logging.handlers.SysLogHandler.LOG_DAEMON, socktype=socktype + ) else: handler = logging.handlers.WatchedFileHandler(log_path) fmt = logging.Formatter(logging.BASIC_FORMAT, None)