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:
Amnon Heiman
2018-05-31 15:41:56 +03:00
committed by Avi Kivity
parent b5e42bc6a0
commit bc7503feee
5 changed files with 90 additions and 29 deletions

View File

@@ -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()

View File

@@ -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:

View File

@@ -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
View 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()

View File

@@ -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