2015-11-23 20:16:04 +01:00
|
|
|
### 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']
|
2015-11-25 16:56:39 +01:00
|
|
|
self.stats_url_username = params['stats_url_username']
|
|
|
|
self.stats_url_password = params['stats_url_password']
|
2015-11-23 20:16:04 +01:00
|
|
|
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
|
2015-11-25 16:56:39 +01:00
|
|
|
def _get_couchdb_stats(url, username, password, refresh_rate):
|
2015-11-23 20:16:04 +01:00
|
|
|
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'
|
|
|
|
|
2015-11-25 16:56:39 +01:00
|
|
|
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)
|
2015-11-23 20:16:04 +01:00
|
|
|
# Set time out for urlopen to 2 seconds otherwise we run into the possibility of hosing gmond
|
2015-11-25 16:56:39 +01:00
|
|
|
c = urllib2.urlopen(request, None, 2)
|
2015-11-23 20:16:04 +01:00
|
|
|
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))
|
2015-11-25 16:56:39 +01:00
|
|
|
data = UpdateCouchdbThread._get_couchdb_stats(self.stats_url, self.stats_url_username, self.stats_url_password, self.refresh_rate)
|
2015-11-23 20:16:04 +01:00
|
|
|
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')
|
2015-11-25 16:56:39 +01:00
|
|
|
parser.add_option('-U', '--user', dest='stats_url_username', default='')
|
|
|
|
parser.add_option('-P', '--password', dest='stats_url_password', default='')
|
2015-11-23 20:16:04 +01:00
|
|
|
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,
|
2015-11-25 16:56:39 +01:00
|
|
|
'stats_url_username': options.stats_url_username,
|
|
|
|
'stats_url_password': options.stats_url_password,
|
2015-11-23 20:16:04 +01:00
|
|
|
'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()
|