###  This script reports couchdb metrics to ganglia.

###  License to use, modify, and distribute under the GPL
###  http://www.gnu.org/licenses/gpl.txt
import logging
import os
import subprocess
import sys
import threading
import time
import traceback
import urllib2
import json

logging.basicConfig(level=logging.ERROR)

_Worker_Thread = None

class UpdateCouchdbThread(threading.Thread):

    def __init__(self, params):
        threading.Thread.__init__(self)
        self.running = False
        self.shuttingdown = False
        self.refresh_rate = int(params['refresh_rate'])
        self.metrics = {}
        self.settings = {}
        self.stats_url = params['stats_url']
        self.stats_url_username = params['stats_url_username']
        self.stats_url_password = params['stats_url_password']
        self._metrics_lock = threading.Lock()
        self._settings_lock = threading.Lock()

    def shutdown(self):
        self.shuttingdown = True
        if not self.running:
            return
        self.join()

    def run(self):
        global _Lock

        self.running = True

        while not self.shuttingdown:
            time.sleep(self.refresh_rate)
            self.refresh_metrics()

        self.running = False

    @staticmethod
    def _get_couchdb_stats(url, username, password, refresh_rate):
        if refresh_rate == 60 or refresh_rate == 300 or refresh_rate == 900:
            url += '?range=' + str(refresh_rate)
        else:
            logging.warning('The specified refresh_rate of %d is invalid and has been substituted with 60!' % refresh_rate)
            url += '?range=60'

        if username != "":
            passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
            passman.add_password(None, url, username, password)
            urllib2.install_opener(urllib2.build_opener(urllib2.HTTPBasicAuthHandler(passman)))

        request = urllib2.Request(url)
        # Set time out for urlopen to 2 seconds otherwise we run into the possibility of hosing gmond
        c = urllib2.urlopen(request, None, 2)
        json_data = c.read()
        c.close()

        data = json.loads(json_data)
        couchdb = data['couchdb']
        httpd = data['httpd']
        request_methods = data['httpd_request_methods']
        status_codes = data['httpd_status_codes']

        result = {}
        for first_level_key in data:
            for second_level_key in data[first_level_key]:
                value = data[first_level_key][second_level_key]['current']
                if value is None:
                    value = 0
                else:
                    if second_level_key in ['open_databases', 'open_os_files', 'clients_requesting_changes']:
                        print second_level_key + ': ' + str(value)
                        value = int(value)
                    else:
                        # We need to devide by the range as couchdb provides no per second values
                        value = float(value) / refresh_rate
                result['couchdb_' + first_level_key + '_' + second_level_key ] = value

        return result

    def refresh_metrics(self):
        logging.debug('refresh metrics')

        try:
            logging.debug(' opening URL: ' + str(self.stats_url))
            data = UpdateCouchdbThread._get_couchdb_stats(self.stats_url, self.stats_url_username, self.stats_url_password, self.refresh_rate)
        except:
            logging.warning('error refreshing metrics')
            logging.warning(traceback.print_exc(file=sys.stdout))

        try:
            self._metrics_lock.acquire()
            self.metrics = {}
            for k, v in data.items():
                self.metrics[k] = v
        except:
            logging.warning('error refreshing metrics')
            logging.warning(traceback.print_exc(file=sys.stdout))
            return False

        finally:
            self._metrics_lock.release()

        if not self.metrics:
            logging.warning('error refreshing metrics')
            return False

        logging.debug('success refreshing metrics')
        logging.debug('metrics: ' + str(self.metrics))

        return True

    def metric_of(self, name):
        logging.debug('getting metric: ' + name)

        try:
            if name in self.metrics:
                try:
                    self._metrics_lock.acquire()
                    logging.debug('metric: %s = %s' % (name, self.metrics[name]))
                    return self.metrics[name]
                finally:
                    self._metrics_lock.release()
        except:
            logging.warning('failed to fetch ' + name)
            return 0

    def setting_of(self, name):
        logging.debug('getting setting: ' + name)

        try:
            if name in self.settings:
                try:
                    self._settings_lock.acquire()
                    logging.debug('setting: %s = %s' % (name, self.settings[name]))
                    return self.settings[name]
                finally:
                    self._settings_lock.release()
        except:
            logging.warning('failed to fetch ' + name)
            return 0

def metric_init(params):
    logging.debug('init: ' + str(params))
    global _Worker_Thread

    METRIC_DEFAULTS = {
        'units': 'requests/s',
        'groups': 'couchdb',
        'slope': 'both',
        'value_type': 'float',
        'format': '%.3f',
        'description': '',
        'call_back': metric_of
    }

    descriptions = dict(
        couchdb_couchdb_auth_cache_hits={
            'units': 'hits/s',
            'description': 'Number of authentication cache hits'},
        couchdb_couchdb_auth_cache_misses={
            'units': 'misses/s',
            'description': 'Number of authentication cache misses'},
        couchdb_couchdb_database_reads={
            'units': 'reads/s',
            'description': 'Number of times a document was read from a database'},
        couchdb_couchdb_database_writes={
            'units': 'writes/s',
            'description': 'Number of times a document was changed'},
        couchdb_couchdb_open_databases={
            'value_type': 'uint',
            'format': '%d',
            'units': 'databases',
            'description': 'Number of open databases'},
        couchdb_couchdb_open_os_files={
            'value_type': 'uint',
            'format': '%d',
            'units': 'files',
            'description': 'Number of file descriptors CouchDB has open'},
        couchdb_couchdb_request_time={
            'units': 'ms',
            'description': 'Request time'},
        couchdb_httpd_bulk_requests={
            'description': 'Number of bulk requests'},
        couchdb_httpd_clients_requesting_changes={
            'value_type': 'uint',
            'format': '%d',
            'units': 'clients',
            'description': 'Number of clients for continuous _changes'},
        couchdb_httpd_requests={
            'description': 'Number of HTTP requests'},
        couchdb_httpd_temporary_view_reads={
            'units': 'reads',
            'description': 'Number of temporary view reads'},
        couchdb_httpd_view_reads={
            'description': 'Number of view reads'},
        couchdb_httpd_request_methods_COPY={
            'description': 'Number of HTTP COPY requests'},
        couchdb_httpd_request_methods_DELETE={
            'description': 'Number of HTTP DELETE requests'},
        couchdb_httpd_request_methods_GET={
            'description': 'Number of HTTP GET requests'},
        couchdb_httpd_request_methods_HEAD={
            'description': 'Number of HTTP HEAD requests'},
        couchdb_httpd_request_methods_POST={
            'description': 'Number of HTTP POST requests'},
        couchdb_httpd_request_methods_PUT={
            'description': 'Number of HTTP PUT requests'},
        couchdb_httpd_status_codes_200={
            'units': 'responses/s',
            'description': 'Number of HTTP 200 OK responses'},
        couchdb_httpd_status_codes_201={
            'units': 'responses/s',
            'description': 'Number of HTTP 201 Created responses'},
        couchdb_httpd_status_codes_202={
            'units': 'responses/s',
            'description': 'Number of HTTP 202 Accepted responses'},
        couchdb_httpd_status_codes_301={
            'units': 'responses/s',
            'description': 'Number of HTTP 301 Moved Permanently responses'},
        couchdb_httpd_status_codes_304={
            'units': 'responses/s',
            'description': 'Number of HTTP 304 Not Modified responses'},
        couchdb_httpd_status_codes_400={
            'units': 'responses/s',
            'description': 'Number of HTTP 400 Bad Request responses'},
        couchdb_httpd_status_codes_401={
            'units': 'responses/s',
            'description': 'Number of HTTP 401 Unauthorized responses'},
        couchdb_httpd_status_codes_403={
            'units': 'responses/s',
            'description': 'Number of HTTP 403 Forbidden responses'},
        couchdb_httpd_status_codes_404={
            'units': 'responses/s',
            'description': 'Number of HTTP 404 Not Found responses'},
        couchdb_httpd_status_codes_405={
            'units': 'responses/s',
            'description': 'Number of HTTP 405 Method Not Allowed responses'},
        couchdb_httpd_status_codes_409={
            'units': 'responses/s',
            'description': 'Number of HTTP 409 Conflict responses'},
        couchdb_httpd_status_codes_412={
            'units': 'responses/s',
            'description': 'Number of HTTP 412 Precondition Failed responses'},
        couchdb_httpd_status_codes_500={
            'units': 'responses/s',
            'description': 'Number of HTTP 500 Internal Server Error responses'})

    if _Worker_Thread is not None:
        raise Exception('Worker thread already exists')

    _Worker_Thread = UpdateCouchdbThread(params)
    _Worker_Thread.refresh_metrics()
    _Worker_Thread.start()

    descriptors = []

    for name, desc in descriptions.iteritems():
        d = desc.copy()
        d['name'] = str(name)
        [ d.setdefault(key, METRIC_DEFAULTS[key]) for key in METRIC_DEFAULTS.iterkeys() ]
        descriptors.append(d)

    return descriptors

def metric_of(name):
    global _Worker_Thread
    return _Worker_Thread.metric_of(name)

def setting_of(name):
    global _Worker_Thread
    return _Worker_Thread.setting_of(name)

def metric_cleanup():
    global _Worker_Thread
    if _Worker_Thread is not None:
        _Worker_Thread.shutdown()
    logging.shutdown()
    pass

if __name__ == '__main__':
    from optparse import OptionParser

    try:
        logging.debug('running from the cmd line')
        parser = OptionParser()
        parser.add_option('-u', '--URL', dest='stats_url', default='http://127.0.0.1:5984/_stats', help='URL for couchdb stats page')
        parser.add_option('-U', '--user', dest='stats_url_username', default='')
        parser.add_option('-P', '--password', dest='stats_url_password', default='')
        parser.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False)
        parser.add_option('-r', '--refresh-rate', dest='refresh_rate', default=60)
        parser.add_option('-d', '--debug', dest='debug', action='store_true', default=False)

        (options, args) = parser.parse_args()

        descriptors = metric_init({
            'stats_url': options.stats_url,
            'stats_url_username': options.stats_url_username,
            'stats_url_password': options.stats_url_password,
            'refresh_rate': options.refresh_rate
        })

        if options.debug:
            from pprint import pprint
            pprint(descriptors)

        for d in descriptors:
            v = d['call_back'](d['name'])

            if not options.quiet:
                print ' {0}: {1} {2} [{3}]' . format(d['name'], v, d['units'], d['description'])

        os._exit(1)

    except KeyboardInterrupt:
        time.sleep(0.2)
        os._exit(1)
    except StandardError:
        traceback.print_exc()
        os._exit(1)
    finally:
        metric_cleanup()