#!/usr/bin/env python """ Loggly CLI interface - standalone (Just Python). Deals with paging results automatically so you can easily grab >2000 rows. Run `loggly.py --help` to see usage information. TODO: * Support XML format results (ie. combining multiple pages of results together) * Facet support Rob Coup - @amatix - robert.coup@koordinates.com ------- Copyright (c) 2012, Robert Coup All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. """ from optparse import OptionParser import urllib import urllib2 import os import json import sys import time def main(): parser = OptionParser("Usage: %prog [options] SEARCH_QUERY") parser.add_option("--username", help="Loggly username. Or specify via LOGGLY_USERNAME environment variable") parser.add_option("--password", help="Loggly password. Or specify via LOGGLY_PASSWORD") parser.add_option("--subdomain", help="Loggly subdomain (eg. foo). Or specify via LOGGLY_SUBDOMAIN") parser.add_option("--from", help="Start time for the search.", default="NOW-24HOURS", dest="start") parser.add_option("--to", help="End time for the search.", default="NOW", dest="end") parser.add_option("--format", help="Output format, either 'json', 'xml', 'csv', or 'text'.", choices=('text', 'json', 'csv', 'xml'), default="text") parser.add_option("--order", help="Direction of results returned, either 'asc' or 'desc'.", choices=('asc', 'desc'), default="asc") # search API parser.add_option("--rows", help="Number of rows returned by search. 0 will get all matching rows via paging.", type="int", default=0) options, args = parser.parse_args() if len(args) != 1: parser.error("Need to specify SEARCH_QUERY as one argument") if not options.username and 'LOGGLY_USERNAME' in os.environ: options.username = os.environ['LOGGLY_USERNAME'] if not options.password and 'LOGGLY_PASSWORD' in os.environ: options.password = os.environ['LOGGLY_PASSWORD'] if not options.subdomain and 'LOGGLY_SUBDOMAIN' in os.environ: options.subdomain = os.environ['LOGGLY_SUBDOMAIN'] if not options.username: parser.error("Need to specify either LOGGLY_USERNAME or --username") if not options.password: parser.error("Need to specify either LOGGLY_PASSWORD or --password") if not options.subdomain: parser.error("Need to specify either LOGGLY_SUBDOMAIN or --subdomain") url = "https://%s.loggly.com/api/search/" % options.subdomain password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() password_mgr.add_password(None, url, options.username, options.password) handler = urllib2.HTTPBasicAuthHandler(password_mgr) opener = urllib2.build_opener(handler) if options.rows > 2000 or options.rows <= 0: target_rows = None if options.rows <= 0 else options.rows rows = 2000 paging = True else: rows = options.rows paging = False params = { 'q': args[0], 'from': options.start, 'until': options.end, 'format': options.format, 'order': options.order, 'rows': rows, 'start': 0, } results = None while True: req_url = url + '?' + urllib.urlencode(params) print >>sys.stderr, req_url try: resp = opener.open(req_url) except Exception, e: print >>sys.stderr, "\t%s -- retrying" % repr(e) time.sleep(3) resp = opener.open(req_url) result_count = 0 if options.format in ('csv', 'text'): # can stream the output for line in resp: if len(line) > 1: result_count += 1 print line, elif options.format == 'json': # merge JSON result pages in. r_this = json.load(resp) result_count += len(r_this['data']) if results is None: results = r_this else: results['data'] += r_this['data'] elif options.format == 'xml': # TODO: merge XML result pages together print resp.read() raise NotImplementedError("TODO: implement XML-format result paging") if paging: if target_rows: got = params['start'] + result_count if got >= target_rows: # got the target number of rows break params['rows'] += min(2000, target_rows - got) elif result_count == 0: # no more results break params['start'] += 2000 else: # no paging, just get required break if options.format == 'json': json.dump(results, sys.stdout, indent=2) sys.stdout.write("\n") elif options.format == 'xml': # TODO: output combined XML results raise NotImplementedError("TODO: implement XML-format result paging") if __name__ == "__main__": main()