summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md11
-rwxr-xr-xbin/logster66
-rw-r--r--logster/logster_helper.py101
-rw-r--r--tests/test_cloudwatch.py35
4 files changed, 192 insertions, 21 deletions
diff --git a/README.md b/README.md
index 572e169..e20adcf 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Logster - generate metrics from logfiles [![Build Status](https://secure.travis-ci.org/etsy/logster.png)](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()
+