diff options
-rw-r--r-- | README.md | 11 | ||||
-rwxr-xr-x | bin/logster | 66 | ||||
-rw-r--r-- | logster/logster_helper.py | 101 | ||||
-rw-r--r-- | tests/test_cloudwatch.py | 35 |
4 files changed, 192 insertions, 21 deletions
@@ -1,7 +1,7 @@ # Logster - generate metrics from logfiles [](http://travis-ci.org/etsy/logster) Logster is a utility for reading log files and generating metrics in Graphite -or Ganglia. It is ideal for visualizing trends of events that are occurring in +or Ganglia or Amazon CloudWatch. It is ideal for visualizing trends of events that are occurring in your application/system/error logs. For example, you might use logster to graph the number of occurrences of HTTP response code that appears in your web server logs. @@ -9,14 +9,14 @@ logs. Logster maintains a cursor, via logtail, on each log file that it reads so that each successive execution only inspects new log entries. In other words, a 1 minute crontab entry for logster would allow you to generate near real-time -trends in Graphite or Ganglia for anything you want to measure from your logs. +trends in Graphite or Ganglia or Amazon CloudWatch for anything you want to measure from your logs. This tool is made up of a framework script, logster, and parsing scripts that are written to accommodate your specific log format. Two sample parsers are included in this distribution. The parser scripts essentially read a log file line by line, apply a regular expression to extract useful data from the lines you are interested in, and then aggregate that data into metrics that will be -submitted to either Ganglia or Graphite. Take a look through the sample +submitted to Ganglia or Graphite or Amazon CloudWatch. Take a look through the sample parsers, which should give you some idea of how to get started writing your own. @@ -57,7 +57,7 @@ You can test logster from the command line. There are two sample parsers: SampleLogster, which generates stats from an Apache access log; and Log4jLogster, which generates stats from a log4j log. The --dry-run option will allow you to see the metrics being generated on stdout rather than sending them -to either Ganglia or Graphite. +to Ganglia or Graphite or Amazon CloudWatch. $ sudo /usr/sbin/logster --dry-run --output=ganglia SampleLogster /var/log/httpd/access_log @@ -101,6 +101,9 @@ Additional usage details can be found with the -h option: --graphite-host=GRAPHITE_HOST Hostname and port for Graphite collector, e.g. graphite.example.com:2003 + --aws-key=AWS_KEY Amazon credential key + --aws-secret-key=AWS_SECRET_KEY + Amazon credential secret key -s STATE_DIR, --state-dir=STATE_DIR Where to store the logtail state file. Default location /var/run diff --git a/bin/logster b/bin/logster index 12edb38..29901cf 100755 --- a/bin/logster +++ b/bin/logster @@ -51,11 +51,11 @@ import fcntl import socket import traceback -from time import time +from time import time, strftime, gmtime from math import floor # Local dependencies -from logster.logster_helper import LogsterParsingException, LockingError +from logster.logster_helper import LogsterParsingException, LockingError, CloudWatch, CloudWatchException # Globals gmetric = "/usr/bin/gmetric" @@ -85,11 +85,15 @@ cmdline.add_option('--gmetric-options', action='store', default='-d 180 -c /etc/ganglia/gmond.conf') cmdline.add_option('--graphite-host', action='store', help='Hostname and port for Graphite collector, e.g. graphite.example.com:2003') +cmdline.add_option('--aws-key', action='store', default=os.getenv('AWS_ACCESS_KEY_ID'), + help='Amazon credential key') +cmdline.add_option('--aws-secret-key', action='store', default=os.getenv('AWS_SECRET_ACCESS_KEY_ID'), + help='Amazon credential secret key') cmdline.add_option('--state-dir', '-s', action='store', default=state_dir, help='Where to store the logtail state file. Default location %s' % state_dir) cmdline.add_option('--output', '-o', action='append', - choices=('graphite', 'ganglia', 'stdout'), - help="Where to send metrics (can specify multiple times). Choices are 'graphite', 'ganglia', or 'stdout'.") + choices=('graphite', 'ganglia', 'stdout', 'cloudwatch'), + help="Where to send metrics (can specify multiple times). Choices are 'graphite', 'ganglia', 'cloudwatch' or 'stdout'.") cmdline.add_option('--stdout-separator', action='store', default="_", dest="stdout_separator", help='Seperator between prefix/suffix and name for stdout. Default is \"%default\".') cmdline.add_option('--dry-run', '-d', action='store_true', default=False, @@ -110,6 +114,9 @@ if not options.output: if 'graphite' in options.output and not options.graphite_host: cmdline.print_help() cmdline.error("You must supply --graphite-host when using 'graphite' as an output type.") +if 'cloudwatch' in options.output and not options.aws_key and not options.aws_secret_key: + cmdline.print_help() + cmdline.error("You must supply --aws-key and --aws-secret-key or Set environment variables. AWS_ACCESS_KEY_ID for --aws-key, AWS_SECRET_ACCESS_KEY_ID for --aws-secret-key") class_name = arguments[0] if class_name.find('.') == -1: @@ -154,6 +161,8 @@ def submit_stats(parser, duration, options): submit_graphite(metrics, options) if 'stdout' in options.output: submit_stdout(metrics, options) + if 'cloudwatch' in options.output: + submit_cloudwatch(metrics, options) def submit_stdout(metrics, options): for metric in metrics: @@ -161,7 +170,7 @@ def submit_stdout(metrics, options): metric.name = options.metric_prefix + options.stdout_separator + metric.name if (options.metric_suffix is not None): metric.name = metric.name + options.stdout_separator + options.metric_suffix - print "%s %s" %(metric.name, metric.value) + print "%s %s %s" %(metric.timestamp, metric.name, metric.value) def submit_ganglia(metrics, options): for metric in metrics: @@ -190,24 +199,52 @@ def submit_graphite(metrics, options): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host[0], int(host[1]))) - for metric in metrics: + try: + for metric in metrics: + + if (options.metric_prefix != ""): + metric.name = options.metric_prefix + "." + metric.name + if (options.metric_suffix is not None): + metric.name = metric.name + "." + options.metric_suffix + + metric_string = "%s %s %s" % (metric.name, metric.value, metric.timestamp) + logger.debug("Submitting Graphite metric: %s" % metric_string) + + if (not options.dry_run): + s.sendall("%s\n" % metric_string) + else: + print "%s %s" % (options.graphite_host, metric_string) + finally: + if (not options.dry_run): + s.close() +def submit_cloudwatch(metrics, options): + for metric in metrics: + if (options.metric_prefix != ""): metric.name = options.metric_prefix + "." + metric.name if (options.metric_suffix is not None): metric.name = metric.name + "." + options.metric_suffix - + + metric.timestamp = strftime("%Y%m%dT%H:%M:00Z", gmtime(metric.timestamp)) + metric.units = "None" metric_string = "%s %s %s" % (metric.name, metric.value, metric.timestamp) - logger.debug("Submitting Graphite metric: %s" % metric_string) + logger.debug("Submitting CloudWatch metric: %s" % metric_string) if (not options.dry_run): - s.send("%s\n" % metric_string) - else: - print "%s %s" % (options.graphite_host, metric_string) - - if (not options.dry_run): - s.close() + try: + cw = CloudWatch(options.aws_key, options.aws_secret_key, metric).get_instance_id() + except CloudWatchException: + logger.debug("Is this machine really amazon EC2?") + sys.exit(1) + try: + cw.put_data() + except CloudWatchException, e: + logger.debug(e.message) + sys.exit(1) + else: + print metric_string def start_locking(lockfile_name): """ Acquire a lock via a provided lockfile filename. """ @@ -343,4 +380,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/logster/logster_helper.py b/logster/logster_helper.py index c8b3ca1..a0d2487 100644 --- a/logster/logster_helper.py +++ b/logster/logster_helper.py @@ -19,6 +19,21 @@ ### along with Logster. If not, see <http://www.gnu.org/licenses/>. ### +try: + from httplib import * +except ImportError: + from http.client import * + +import base64 +import hashlib +import hmac +import sys + +try: + from urllib import urlencode, quote_plus +except ImportError: + from urllib.parse import urlencode, quote_plus + from time import time class MetricObject(object): @@ -34,11 +49,11 @@ class LogsterParser(object): """Base class for logster parsers""" def parse_line(self, line): """Take a line and do any parsing we need to do. Required for parsers""" - raise RuntimeError, "Implement me!" + raise RuntimeError("Implement me!") def get_state(self, duration): """Run any calculations needed and return list of metric objects""" - raise RuntimeError, "Implement me!" + raise RuntimeError("Implement me!") class LogsterParsingException(Exception): @@ -51,3 +66,85 @@ class LockingError(Exception): """ Exception raised for errors creating or destroying lockfiles. """ pass +class CloudWatchException(Exception): + """ Raise thie exception if the connection can't be established + with Amazon server """ + pass + +class CloudWatch: + """ Base class for Amazon CloudWatch """ + def __init__(self, key, secret_key, metric): + """ Specify Amazon CloudWatch params """ + + self.base_url = "monitoring.ap-northeast-1.amazonaws.com" + self.key = key + self.secret_key = secret_key + self.metric = metric + + def get_instance_id(self, instance_id = None): + """ get instance id from amazon meta data server """ + + self.instance_id = instance_id + + if self.instance_id is None: + try: + conn = HTTPConnection("169.254.169.254") + conn.request("GET", "/latest/meta-data/instance-id") + except Exception: + raise CloudWatchException("Can't connect Amazon meta data server to get InstanceID : (%s)") + + self.instance_id = conn.getresponse().read() + + return self + + def set_params(self): + + params = {'Namespace': 'logster', + 'MetricData.member.1.MetricName': self.metric.name, + 'MetricData.member.1.Value': self.metric.value, + 'MetricData.member.1.Unit': self.metric.units, + 'MetricData.member.1.Dimensions.member.1.Name': 'InstanceID', + 'MetricData.member.1.Dimensions.member.1.Value': self.instance_id} + + self.url_params = params + self.url_params['AWSAccessKeyId'] = self.key + self.url_params['Action'] = 'PutMetricData' + self.url_params['SignatureMethod'] = 'HmacSHA256' + self.url_params['SignatureVersion'] = '2' + self.url_params['Version'] = '2010-08-01' + self.url_params['Timestamp'] = self.metric.timestamp + + return self + + def get_signed_url(self): + """ build signed parameters following + http://docs.amazonwebservices.com/AmazonCloudWatch/latest/APIReference/API_PutMetricData.html """ + keys = sorted(self.url_params) + values = map(self.url_params.get, keys) + url_string = urlencode(list(zip(keys,values))) + + string_to_sign = "GET\n%s\n/\n%s" % (self.base_url, url_string) + try: + if sys.version_info[:2] == (2, 5): + signature = hmac.new( key=self.secret_key, msg=string_to_sign, digestmod=hashlib.sha256).digest() + else: + signature = hmac.new( key=bytes(self.secret_key), msg=bytes(string_to_sign), digestmod=hashlib.sha256).digest() + except TypeError: + signature = hmac.new( key=bytes(self.secret_key, "utf-8"), msg=bytes(string_to_sign, "utf-8"), digestmod=hashlib.sha256).digest() + + signature = base64.encodestring(signature).strip() + urlencoded_signature = quote_plus(signature) + url_string += "&Signature=%s" % urlencoded_signature + + return "/?" + url_string + + def put_data(self): + signedURL = self.set_params().get_signed_url() + try: + conn = HTTPConnection(self.base_url) + conn.request("GET", signedURL) + except Exception: + raise CloudWatchException("Can't connect Amazon CloudWatch server") + res = conn.getresponse() + + diff --git a/tests/test_cloudwatch.py b/tests/test_cloudwatch.py new file mode 100644 index 0000000..6c17e21 --- /dev/null +++ b/tests/test_cloudwatch.py @@ -0,0 +1,35 @@ +from logster.logster_helper import CloudWatch, MetricObject +from time import time, strftime, gmtime +import unittest + +class TestCloudWatch(unittest.TestCase): + + def setUp(self): + + self.metric = MetricObject("ERROR", 1, None) + self.metric.timestamp = strftime("%Y%m%dT%H:%M:00Z", gmtime(self.metric.timestamp)) + + self.cw = CloudWatch("key", "secretkey", self.metric) + self.cw.get_instance_id("myserverID").set_params().get_signed_url() + + def test_params(self): + + self.assertEqual(self.cw.base_url, "monitoring.ap-northeast-1.amazonaws.com") + self.assertEqual(self.cw.key, "key") + self.assertEqual(self.cw.secret_key, "secretkey") + self.assertEqual(self.cw.url_params['Namespace'], "logster") + self.assertEqual(self.cw.url_params['MetricData.member.1.MetricName'], "ERROR") + self.assertEqual(self.cw.url_params['MetricData.member.1.Value'], 1) + self.assertEqual(self.cw.url_params['MetricData.member.1.Unit'], None) + self.assertEqual(self.cw.url_params['MetricData.member.1.Dimensions.member.1.Name'], "InstanceID") + self.assertEqual(self.cw.url_params['MetricData.member.1.Dimensions.member.1.Value'], "myserverID") + self.assertEqual(self.cw.url_params['AWSAccessKeyId'], "key") + self.assertEqual(self.cw.url_params['Timestamp'], self.metric.timestamp) + self.assertEqual(self.cw.url_params['Action'], 'PutMetricData') + self.assertEqual(self.cw.url_params['SignatureMethod'], 'HmacSHA256') + self.assertEqual(self.cw.url_params['SignatureVersion'], '2') + self.assertEqual(self.cw.url_params['Version'], '2010-08-01') + +if __name__ == '__main__': + unittest.main() + |