Scyllatop to use prometheus by default
Scylla now expose the prometheus API by default. This patch chagnes scyllatop to use the Prometheus API, the collect API is still available. The main changes in the patch: * Move collectd specific logic inside collectd. * Add support for help information. * Add command line to configure prometheus end point and to enable collectd. * Add a prometheus class that collect information from prometheus. Fixes: #1541 Message-Id: <20180531124156.26336-1-amnon@scylladb.com>
This commit is contained in:
@@ -17,6 +17,9 @@ COLLECTD_EXAMPLE_CONFIGURATION = '\n'.join(['LoadPlugin unixsock',
|
||||
|
||||
class Collectd(object):
|
||||
_FIRST_LINE_PATTERN = re.compile('^(?P<lines>\d+)')
|
||||
_METRIC_INFO_PATTERN = re.compile('^(?P<key>[^=]+)=(?P<value>.*)$')
|
||||
_METRIC_DISCOVER_PATTERN_WITH_HELP = re.compile('[^ ]+ (?P<metric>.+)(?P<help>.*)$')
|
||||
_METRIC_DISCOVER_PATTERN = re.compile('[^ ]+ (?P<metric>.+)(?P<help>.*)$')
|
||||
|
||||
def __init__(self, socketName):
|
||||
try:
|
||||
@@ -32,7 +35,13 @@ class Collectd(object):
|
||||
self._socket.connect(socketName)
|
||||
self._lineReader = os.fdopen(self._socket.fileno())
|
||||
|
||||
def query(self, command):
|
||||
def query_val(self, val):
|
||||
return self.internal_query('GETVAL "{metric}"'.format(metric=val))
|
||||
|
||||
def query_list(self):
|
||||
return self.internal_query('LISTVAL')
|
||||
|
||||
def internal_query(self, command):
|
||||
self._send(command)
|
||||
return self._readLines()
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ import defaults
|
||||
|
||||
|
||||
class LiveData(object):
|
||||
def __init__(self, metricPatterns, interval, collectd):
|
||||
logging.info('will query collectd every {0} seconds'.format(interval))
|
||||
def __init__(self, metricPatterns, interval, metric_source):
|
||||
logging.info('will query metric_source every {0} seconds'.format(interval))
|
||||
self._startedAt = time.time()
|
||||
self._measurements = []
|
||||
self._interval = interval
|
||||
self._collectd = collectd
|
||||
self._metric_source = metric_source
|
||||
self._initializeMetrics(metricPatterns)
|
||||
self._views = []
|
||||
self._stop = False
|
||||
@@ -31,11 +31,11 @@ class LiveData(object):
|
||||
self._setupUserSpecifiedMetrics(defaults.DEFAULT_METRIC_PATTERNS)
|
||||
|
||||
def _setupUserSpecifiedMetrics(self, metricPatterns):
|
||||
availableSymbols = [m.symbol for m in metric.Metric.discover(self._collectd)]
|
||||
availableSymbols = [m.symbol for m in metric.Metric.discover(self._metric_source)]
|
||||
symbols = [symbol for symbol in availableSymbols if self._matches(symbol, metricPatterns)]
|
||||
for symbol in symbols:
|
||||
logging.info('adding {0}'.format(symbol))
|
||||
self._measurements.append(metric.Metric(symbol, self._collectd))
|
||||
self._measurements.append(metric.Metric(symbol, self._metric_source, ""))
|
||||
|
||||
def _matches(self, symbol, metricPatterns):
|
||||
for pattern in metricPatterns:
|
||||
|
||||
@@ -4,51 +4,70 @@ import parseexception
|
||||
|
||||
|
||||
class Metric(object):
|
||||
_METRIC_SYMBOL_HOST_PATTERN = re.compile('^[^/]+/')
|
||||
_METRIC_INFO_PATTERN = re.compile('^(?P<key>[^=]+)=(?P<value>.*)$')
|
||||
_METRIC_DISCOVER_PATTERN = re.compile('[^ ]+ (?P<metric>.+)$')
|
||||
|
||||
def __init__(self, symbol, collectd):
|
||||
def __init__(self, symbol, metric_source, hlp):
|
||||
self._symbol = symbol
|
||||
self._collectd = collectd
|
||||
self._metric_source = metric_source
|
||||
self._status = {}
|
||||
self._help_line = hlp
|
||||
|
||||
@property
|
||||
def symbol(self):
|
||||
return self._symbol
|
||||
|
||||
@property
|
||||
def help(self):
|
||||
return self._help_line
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
def update(self):
|
||||
response = self._collectd.query('GETVAL "{metric}"'.format(metric=self._symbol))
|
||||
response = self._metric_source.query_val(self._symbol)
|
||||
if response is None:
|
||||
self.markAbsent()
|
||||
return
|
||||
for line in response:
|
||||
match = self._METRIC_INFO_PATTERN.search(line)
|
||||
match = self._metric_source._METRIC_INFO_PATTERN.search(line)
|
||||
if match is None:
|
||||
raise parseexception.ParseException('could not parse metric pattern from line: {0}'.format(line))
|
||||
key = match.groupdict()['key']
|
||||
value = match.groupdict()['value']
|
||||
self._status[key] = value
|
||||
logging.debug('{}: {}'.format(self.symbol, line.strip()))
|
||||
logging.debug('update {}: {}'.format(self.symbol, line.strip()))
|
||||
|
||||
def markAbsent(self):
|
||||
for key in self._status.keys():
|
||||
self._status[key] = 'not available'
|
||||
|
||||
@classmethod
|
||||
def discover(cls, collectd):
|
||||
def discover(cls, metric_source):
|
||||
results = []
|
||||
logging.info('discovering metrics...')
|
||||
response = collectd.query('LISTVAL')
|
||||
response = metric_source.query_list()
|
||||
for line in response:
|
||||
logging.debug('LISTVAL result: {0}'.format(line))
|
||||
match = cls._METRIC_DISCOVER_PATTERN.search(line)
|
||||
metric = match.groupdict()['metric']
|
||||
results.append(Metric(metric, collectd))
|
||||
match = metric_source._METRIC_DISCOVER_PATTERN.search(line)
|
||||
if match:
|
||||
metric = match.groupdict()['metric']
|
||||
logging.debug('discover list result: {0}'.format(metric))
|
||||
hlp = ""
|
||||
results.append(Metric(metric, metric_source, hlp))
|
||||
|
||||
logging.info('found {0} metrics'.format(len(results)))
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def discover_with_help(cls, metric_source):
|
||||
results = []
|
||||
logging.info('discovering metrics...')
|
||||
response = metric_source.query_list()
|
||||
for line in response:
|
||||
logging.debug('list result: {0}'.format(line))
|
||||
match = metric_source._METRIC_DISCOVER_PATTERN_WITH_HELP.search(line)
|
||||
if match:
|
||||
metric = match.groupdict()['metric']
|
||||
hlp = match.groupdict()['help']
|
||||
results.append(Metric(metric, metric_source, hlp))
|
||||
|
||||
logging.info('found {0} metrics'.format(len(results)))
|
||||
return results
|
||||
|
||||
25
tools/scyllatop/prometheus.py
Executable file
25
tools/scyllatop/prometheus.py
Executable file
@@ -0,0 +1,25 @@
|
||||
#! /usr/bin/python
|
||||
|
||||
import sys
|
||||
import urllib2
|
||||
import re
|
||||
|
||||
class Prometheus(object):
|
||||
_FIRST_LINE_PATTERN = re.compile('^(?P<lines>\d+)')
|
||||
_METRIC_INFO_PATTERN = re.compile('^(?P<key>.+) (?P<value>[^ ]+)[ ]*$')
|
||||
_METRIC_DISCOVER_PATTERN = re.compile('^(?P<metric>[^#].+) (?P<value>[^ ]+)[ ]*$')
|
||||
_METRIC_DISCOVER_PATTERN_WITH_HELP = re.compile('^# HELP (?P<metric>[^ ]+)(?P<help>.*)$')
|
||||
def __init__(self, host):
|
||||
self._host = host
|
||||
|
||||
def read_metrics(self):
|
||||
return urllib2.urlopen(self._host).readlines()
|
||||
|
||||
def get_metrics(self):
|
||||
return self.read_metrics()
|
||||
|
||||
def query_val(self, val):
|
||||
return [l for l in self.get_metrics() if (not l.startswith('#')) and (val=="" or re.match(val, l))]
|
||||
|
||||
def query_list(self):
|
||||
return self.get_metrics()
|
||||
@@ -5,6 +5,7 @@ import threading
|
||||
import pprint
|
||||
import logging
|
||||
import collectd
|
||||
import prometheus
|
||||
import metric
|
||||
import fake
|
||||
import livedata
|
||||
@@ -23,14 +24,14 @@ def shell():
|
||||
logging.error('shell mode requires IPython to be installed')
|
||||
|
||||
|
||||
def fancyUserInterface(metricPatterns, interval, collectd):
|
||||
def fancyUserInterface(metricPatterns, interval, metric_source):
|
||||
aggregateView = views.aggregate.Aggregate()
|
||||
simpleView = views.simple.Simple()
|
||||
userInput = userinput.UserInput()
|
||||
loop = urwid.MainLoop(aggregateView.widget(), unhandled_input=userInput)
|
||||
userInput.setLoop(loop)
|
||||
userInput.setMap(M=aggregateView, S=simpleView)
|
||||
liveData = livedata.LiveData(metricPatterns, interval, collectd)
|
||||
liveData = livedata.LiveData(metricPatterns, interval, metric_source)
|
||||
liveData.addView(simpleView)
|
||||
liveData.addView(aggregateView)
|
||||
liveDataThread = threading.Thread(target=lambda: liveData.go(loop))
|
||||
@@ -43,10 +44,11 @@ def fancyUserInterface(metricPatterns, interval, collectd):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
description = '\n'.join(['A top-like tool for scylladb collectd metrics.',
|
||||
description = '\n'.join(['A top-like tool for scylladb collectd/prometheus metrics.',
|
||||
'Keyboard shortcuts: S - simple view, M - aggregate over multiple cores, Q -quits',
|
||||
'',
|
||||
'You need to configure the unix-sock plugin for collectd'
|
||||
'By default it would work with the Prometheus API and does not require configuration.',
|
||||
'For collectd, you need to configure the unix-sock plugin for collectd'
|
||||
'before you can use this, use the --print-config option to give you a configuration example',
|
||||
'enjoy!'])
|
||||
parser = argparse.ArgumentParser(description=description)
|
||||
@@ -59,10 +61,13 @@ if __name__ == '__main__':
|
||||
parser.add_argument(dest='metricPattern', nargs='*', default=[], help='metrics to query, separated by spaces. You can use shell globs (e.g. *cpu*nice*) here to efficiently specify metrics')
|
||||
parser.add_argument('-i', '--interval', help="time resolution in seconds, default: 1", type=float, default=1)
|
||||
parser.add_argument('-s', '--socket', default='/var/run/collectd-unixsock', help="unixsock plugin to connect to, default: /var/run/collectd-unixsock")
|
||||
parser.add_argument('-p', '--prometheus-address', default='http://localhost:9180/metrics', help="The prometheus end-point")
|
||||
parser.add_argument('--print-config', action='store_true',
|
||||
help="print out a configuration to put in your collectd.conf (you can use -s here to define the socket path)")
|
||||
parser.add_argument('-l', '--list', action='store_true',
|
||||
help="print out a list of all metrics exposed by collectd and exit")
|
||||
parser.add_argument('-c', '--collectd', action='store_true',
|
||||
help="Use collectd instead of Prometheus to connect to scylla")
|
||||
parser.add_argument('-L', '--logfile', default='scyllatop.log',
|
||||
help="specify path for log file")
|
||||
parser.add_argument('-S', '--shell', action='store_true', help="uses IPython to enter a debug shell, usefull for development")
|
||||
@@ -88,18 +93,21 @@ if __name__ == '__main__':
|
||||
|
||||
if arguments.fake:
|
||||
fake.fake()
|
||||
collectd = collectd.Collectd(arguments.socket)
|
||||
if arguments.collectd:
|
||||
metric_source = collectd.Collectd(arguments.socket)
|
||||
else:
|
||||
metric_source = prometheus.Prometheus(arguments.prometheus_address)
|
||||
if arguments.shell:
|
||||
shell()
|
||||
quit()
|
||||
if arguments.list:
|
||||
pprint.pprint([m.symbol for m in metric.Metric.discover(collectd)])
|
||||
pprint.pprint([m.symbol + m.help for m in metric.Metric.discover_with_help(metric_source)])
|
||||
quit()
|
||||
|
||||
try:
|
||||
if not sys.stdout.isatty() or arguments.batch:
|
||||
dumptostdout.dumpToStdout(arguments.metricPattern, arguments.interval, collectd, arguments.iterations)
|
||||
dumptostdout.dumpToStdout(arguments.metricPattern, arguments.interval, metric_source, arguments.iterations)
|
||||
else:
|
||||
fancyUserInterface(arguments.metricPattern, arguments.interval, collectd)
|
||||
fancyUserInterface(arguments.metricPattern, arguments.interval, metric_source)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user