Add proper status page for datasync

or rather, it's a good start..  plenty more could be added
This commit is contained in:
Lance Edgar 2022-08-15 21:06:19 -05:00
parent 839c4e0c28
commit 065f845707
6 changed files with 361 additions and 113 deletions

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2021 Lance Edgar
# Copyright © 2010-2022 Lance Edgar
#
# This file is part of Rattail.
#
@ -31,11 +31,15 @@ import json
import subprocess
import logging
import six
import sqlalchemy as sa
from rattail.db import model
from rattail.datasync.config import load_profiles
from rattail.datasync.util import purge_datasync_settings
from rattail.util import simple_error
from tailbone.views import MasterView
from tailbone.util import raw_datetime
log = logging.getLogger(__name__)
@ -49,11 +53,12 @@ class DataSyncThreadView(MasterView):
index view, with status for each, sort of akin to "dashboard".
For now it only serves the config view.
"""
normalized_model_name = 'datasyncthread'
model_title = "DataSync Thread"
model_title_plural = "DataSync Daemon"
model_key = 'key'
route_prefix = 'datasync'
url_prefix = '/datasync'
listable = False
viewable = False
creatable = False
editable = False
@ -68,26 +73,122 @@ class DataSyncThreadView(MasterView):
'key',
]
def __init__(self, request, context=None):
super(DataSyncThreadView, self).__init__(request, context=context)
app = self.get_rattail_app()
self.datasync_handler = app.get_datasync_handler()
def status(self):
"""
View to list/filter/sort the model data.
If this view receives a non-empty 'partial' parameter in the query
string, then the view will return the rendered grid only. Otherwise
returns the full page.
"""
app = self.get_rattail_app()
model = self.model
try:
process_info = self.datasync_handler.get_supervisor_process_info()
supervisor_error = None
except Exception as error:
process_info = None
supervisor_error = simple_error(error)
profiles = self.datasync_handler.get_configured_profiles()
sql = """
select source, consumer, count(*) as changes
from datasync_change
group by source, consumer
"""
result = self.Session.execute(sql)
all_changes = {}
for row in result:
all_changes[(row.source, row.consumer)] = row.changes
watcher_data = []
consumer_data = []
now = app.localtime()
for key, profile in six.iteritems(profiles):
watcher = profile.watcher
lastrun = self.datasync_handler.get_watcher_lastrun(
watcher.key, local=True, session=self.Session())
status = "okay"
if (now - lastrun).total_seconds() >= (watcher.delay * 2):
status = "dead watcher"
watcher_data.append({
'key': watcher.key,
'spec': profile.watcher_spec,
'dbkey': watcher.dbkey,
'delay': watcher.delay,
'lastrun': raw_datetime(self.rattail_config, lastrun, verbose=True),
'status': status,
})
for consumer in profile.consumers:
if consumer.watcher is watcher:
changes = all_changes.get((watcher.key, consumer.key), 0)
if changes:
oldest = self.Session.query(sa.func.min(model.DataSyncChange.obtained))\
.filter(model.DataSyncChange.source == watcher.key)\
.filter(model.DataSyncChange.consumer == consumer.key)\
.scalar()
oldest = app.localtime(oldest, from_utc=True)
changes = "{} (oldest from {})".format(
changes,
app.render_time_ago(now - oldest))
status = "okay"
if changes:
status = "processing changes"
consumer_data.append({
'key': '{} -> {}'.format(watcher.key, consumer.key),
'spec': consumer.spec,
'dbkey': consumer.dbkey,
'delay': consumer.delay,
'changes': changes,
'status': status,
})
watcher_data.sort(key=lambda w: w['key'])
consumer_data.sort(key=lambda c: c['key'])
context = {
'index_title': "DataSync Status",
'index_url': None,
'process_info': process_info,
'supervisor_error': supervisor_error,
'watcher_data': watcher_data,
'consumer_data': consumer_data,
}
return self.render_to_response('status', context)
def get_data(self, session=None):
data = []
return data
def restart(self):
cmd = self.rattail_config.getlist('tailbone', 'datasync.restart',
# nb. simulate by default
default='/bin/sleep 3')
log.debug("attempting datasync restart with command: %s", cmd)
result = subprocess.call(cmd)
if result == 0:
try:
self.datasync_handler.restart_supervisor_process()
self.request.session.flash("DataSync daemon has been restarted.")
else:
self.request.session.flash("DataSync daemon could not be restarted; result was: {}".format(result), 'error')
return self.redirect(self.request.get_referrer(default=self.request.route_url('datasyncchanges')))
except Exception as error:
self.request.session.flash(simple_error(error), 'error')
return self.redirect(self.request.get_referrer(
default=self.request.route_url('datasyncchanges')))
def configure_get_context(self):
profiles = load_profiles(self.rattail_config,
include_disabled=True,
ignore_problems=True)
profiles = self.datasync_handler.get_configured_profiles(
include_disabled=True,
ignore_problems=True)
profiles_data = []
for profile in sorted(profiles.values(), key=lambda p: p.key):
@ -125,7 +226,12 @@ class DataSyncThreadView(MasterView):
return {
'profiles': profiles,
'profiles_data': profiles_data,
'restart_command': self.rattail_config.get('tailbone', 'datasync.restart'),
'use_profile_settings': self.rattail_config.getbool(
'rattail.datasync', 'use_profile_settings'),
'supervisor_process_name': self.rattail_config.get(
'rattail.datasync', 'supervisor_process_name'),
'restart_command': self.rattail_config.get(
'tailbone', 'datasync.restart'),
'system_user': getpass.getuser(),
}
@ -133,58 +239,67 @@ class DataSyncThreadView(MasterView):
settings = []
watch = []
for profile in json.loads(data['profiles']):
pkey = profile['key']
if profile['enabled']:
watch.append(pkey)
use_profile_settings = data.get('use_profile_settings') == 'true'
settings.append({'name': 'rattail.datasync.use_profile_settings',
'value': 'true' if use_profile_settings else 'false'})
settings.extend([
{'name': 'rattail.datasync.{}.watcher'.format(pkey),
'value': profile['watcher_spec']},
{'name': 'rattail.datasync.{}.watcher.db'.format(pkey),
'value': profile['watcher_dbkey']},
{'name': 'rattail.datasync.{}.watcher.delay'.format(pkey),
'value': profile['watcher_delay']},
{'name': 'rattail.datasync.{}.watcher.retry_attempts'.format(pkey),
'value': profile['watcher_retry_attempts']},
{'name': 'rattail.datasync.{}.watcher.retry_delay'.format(pkey),
'value': profile['watcher_retry_delay']},
{'name': 'rattail.datasync.{}.consumers.runas'.format(pkey),
'value': profile['watcher_default_runas']},
])
if use_profile_settings:
consumers = []
if profile['watcher_consumes_self']:
consumers = ['self']
else:
for profile in json.loads(data['profiles']):
pkey = profile['key']
if profile['enabled']:
watch.append(pkey)
for consumer in profile['consumers_data']:
ckey = consumer['key']
if consumer['enabled']:
consumers.append(ckey)
settings.extend([
{'name': 'rattail.datasync.{}.consumer.{}'.format(pkey, ckey),
'value': consumer['consumer_spec']},
{'name': 'rattail.datasync.{}.consumer.{}.db'.format(pkey, ckey),
'value': consumer['consumer_dbkey']},
{'name': 'rattail.datasync.{}.consumer.{}.delay'.format(pkey, ckey),
'value': consumer['consumer_delay']},
{'name': 'rattail.datasync.{}.consumer.{}.retry_attempts'.format(pkey, ckey),
'value': consumer['consumer_retry_attempts']},
{'name': 'rattail.datasync.{}.consumer.{}.retry_delay'.format(pkey, ckey),
'value': consumer['consumer_retry_delay']},
{'name': 'rattail.datasync.{}.consumer.{}.runas'.format(pkey, ckey),
'value': consumer['consumer_runas']},
])
settings.extend([
{'name': 'rattail.datasync.{}.watcher'.format(pkey),
'value': profile['watcher_spec']},
{'name': 'rattail.datasync.{}.watcher.db'.format(pkey),
'value': profile['watcher_dbkey']},
{'name': 'rattail.datasync.{}.watcher.delay'.format(pkey),
'value': profile['watcher_delay']},
{'name': 'rattail.datasync.{}.watcher.retry_attempts'.format(pkey),
'value': profile['watcher_retry_attempts']},
{'name': 'rattail.datasync.{}.watcher.retry_delay'.format(pkey),
'value': profile['watcher_retry_delay']},
{'name': 'rattail.datasync.{}.consumers.runas'.format(pkey),
'value': profile['watcher_default_runas']},
])
settings.extend([
{'name': 'rattail.datasync.{}.consumers'.format(pkey),
'value': ', '.join(consumers)},
])
consumers = []
if profile['watcher_consumes_self']:
consumers = ['self']
else:
if watch:
settings.append({'name': 'rattail.datasync.watch',
'value': ', '.join(watch)})
for consumer in profile['consumers_data']:
ckey = consumer['key']
if consumer['enabled']:
consumers.append(ckey)
settings.extend([
{'name': 'rattail.datasync.{}.consumer.{}'.format(pkey, ckey),
'value': consumer['consumer_spec']},
{'name': 'rattail.datasync.{}.consumer.{}.db'.format(pkey, ckey),
'value': consumer['consumer_dbkey']},
{'name': 'rattail.datasync.{}.consumer.{}.delay'.format(pkey, ckey),
'value': consumer['consumer_delay']},
{'name': 'rattail.datasync.{}.consumer.{}.retry_attempts'.format(pkey, ckey),
'value': consumer['consumer_retry_attempts']},
{'name': 'rattail.datasync.{}.consumer.{}.retry_delay'.format(pkey, ckey),
'value': consumer['consumer_retry_delay']},
{'name': 'rattail.datasync.{}.consumer.{}.runas'.format(pkey, ckey),
'value': consumer['consumer_runas']},
])
settings.extend([
{'name': 'rattail.datasync.{}.consumers'.format(pkey),
'value': ', '.join(consumers)},
])
if watch:
settings.append({'name': 'rattail.datasync.watch',
'value': ', '.join(watch)})
settings.append({'name': 'rattail.datasync.supervisor_process_name',
'value': data['supervisor_process_name']})
settings.append({'name': 'tailbone.datasync.restart',
'value': data['restart_command']})
@ -204,6 +319,25 @@ class DataSyncThreadView(MasterView):
permission_prefix = cls.get_permission_prefix()
route_prefix = cls.get_route_prefix()
url_prefix = cls.get_url_prefix()
index_title = cls.get_index_title()
# view status
config.add_tailbone_permission(permission_prefix,
'{}.status'.format(permission_prefix),
"View status for DataSync daemon")
# nb. simple 'datasync' route points to 'datasync.status' for now..
config.add_route(route_prefix,
'{}/status/'.format(url_prefix))
config.add_route('{}.status'.format(route_prefix),
'{}/status/'.format(url_prefix))
config.add_view(cls, attr='status',
route_name=route_prefix,
permission='{}.status'.format(permission_prefix))
config.add_view(cls, attr='status',
route_name='{}.status'.format(route_prefix),
permission='{}.status'.format(permission_prefix))
config.add_tailbone_index_page(route_prefix, index_title,
'{}.status'.format(permission_prefix))
# restart
config.add_tailbone_permission(permission_prefix,