Use file modification events instead of signal handler

In case if application is under Apache+mod_wsgi it is not recommended to
use signals [1]. We need to have configuration option for handling
'touch file' event instead of signal.

Alternative solutions:
1) watchdog: can monitor only directories, has issues with eventlet [2].
2) inotify: works only with linux-based systems.

[1]
https://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIRestrictSignal

[2] https://github.com/gorakhargosh/watchdog/issues/332

Change-Id: I6ef02457f21da8e6fbd50e57bfa503b3c31ddd76
Implements: blueprint guru-meditation-report-file-touch
This commit is contained in:
Ivan Kolodyazhny 2015-12-23 15:18:45 +02:00
parent 71ce8f5f30
commit d23e0a65b2
3 changed files with 114 additions and 12 deletions

View File

@ -63,11 +63,15 @@ import inspect
import logging
import os
import signal
import stat
import sys
import threading
import time
import traceback
from oslo_utils import timeutils
from oslo_reports._i18n import _LE
from oslo_reports._i18n import _LW
from oslo_reports.generators import conf as cgen
from oslo_reports.generators import process as prgen
@ -75,6 +79,7 @@ from oslo_reports.generators import threading as tgen
from oslo_reports.generators import version as pgen
from oslo_reports import report
LOG = logging.getLogger(__name__)
@ -84,7 +89,7 @@ class GuruMeditation(object):
This class is a base class for Guru Meditation Reports.
It provides facilities for registering sections and
setting up functionality to auto-run the report on
a certain signal.
a certain signal or use file modification events.
This class should always be used in conjunction with
a Report class via multiple inheritance. It should
@ -124,7 +129,8 @@ class GuruMeditation(object):
This method sets up the Guru Meditation Report to automatically
get dumped to stderr or a file in a given dir when the given signal
is received.
is received. It can also use file modification events instead of
signals.
:param version: the version object for the current product
:param service_name: this program name used to construct logfile name
@ -140,16 +146,52 @@ class GuruMeditation(object):
cls._setup_signal(signum, version, service_name, log_dir)
return
if hasattr(signal, 'SIGUSR1'):
# TODO(dims) We need to remove this in the "O" release cycle
LOG.warning(_LW("Guru mediation now registers SIGUSR1 and SIGUSR2 "
"by default for backward compatibility. SIGUSR1 "
"will no longer be registered in a future "
"release, so please use SIGUSR2 to "
"generate reports."))
cls._setup_signal(signal.SIGUSR1, version, service_name, log_dir)
if hasattr(signal, 'SIGUSR2'):
cls._setup_signal(signal.SIGUSR2, version, service_name, log_dir)
if conf and conf.oslo_reports.file_event_handler:
cls._setup_file_watcher(
conf.oslo_reports.file_event_handler,
conf.oslo_reports.file_event_handler_interval,
version, service_name, log_dir)
else:
if hasattr(signal, 'SIGUSR1'):
# TODO(dims) We need to remove this in the "O" release cycle
LOG.warning(_LW("Guru meditation now registers SIGUSR1 and "
"SIGUSR2 by default for backward "
"compatibility. SIGUSR1 will no longer be "
"registered in a future release, so please "
"use SIGUSR2 to generate reports."))
cls._setup_signal(signal.SIGUSR1,
version, service_name, log_dir)
if hasattr(signal, 'SIGUSR2'):
cls._setup_signal(signal.SIGUSR2,
version, service_name, log_dir)
@classmethod
def _setup_file_watcher(cls, filepath, interval, version, service_name,
log_dir):
st = os.stat(filepath)
if not bool(st.st_mode & stat.S_IRGRP):
LOG.error(_LE("Guru Meditation Report does not have read "
"permissions to '%s' file."), filepath)
def _handler():
mtime = time.time()
while True:
try:
stat = os.stat(filepath)
if stat.st_mtime > mtime:
cls.handle_signal(version, service_name, log_dir, None)
mtime = stat.st_mtime
except OSError:
msg = ("Guru Meditation Report cannot read " +
"'{0}' file".format(filepath))
raise IOError(msg)
finally:
time.sleep(interval)
th = threading.Thread(target=_handler)
th.daemon = True
th.start()
@classmethod
def _setup_signal(cls, signum, version, service_name, log_dir):

View File

@ -27,6 +27,16 @@ _option_group = 'oslo_reports'
_options = [
cfg.StrOpt('log_dir',
help=_('Path to a log directory where to create a file')),
cfg.StrOpt('file_event_handler',
help=_('The path to a file to watch for changes to trigger '
'the reports, instead of signals. Setting this option '
'disables the signal trigger for the reports. If '
'application is running as a WSGI application it is '
'recommended to use this instead of signals.')),
cfg.IntOpt('file_event_handler_interval',
default=1,
help=_('How many seconds to wait between polls when '
'file_event_handler is set'))
]

View File

@ -19,6 +19,7 @@ import os
import re
import signal
import sys
import threading
# needed to get greenthreads
import fixtures
@ -27,8 +28,15 @@ import mock
from oslotest import base
import six
import oslo_config
from oslo_config import fixture
from oslo_reports import guru_meditation_report as gmr
from oslo_reports.models import with_default_views as mwdv
from oslo_reports import opts
CONF = oslo_config.cfg.CONF
opts.set_defaults(CONF)
class FakeVersionObj(object):
@ -51,6 +59,24 @@ def skip_body_lines(start_line, report_lines):
return curr_line
class GmrConfigFixture(fixture.Config):
def setUp(self):
super(GmrConfigFixture, self).setUp()
self.conf.set_override(
'file_event_handler',
'/tmp/file',
group='oslo_reports')
self.conf.set_override(
'file_event_handler_interval',
10,
group='oslo_reports')
self.conf.set_override(
'log_dir',
'/var/fake_log',
group='oslo_reports')
class TestGuruMeditationReport(base.BaseTestCase):
def setUp(self):
super(TestGuruMeditationReport, self).setUp()
@ -61,6 +87,8 @@ class TestGuruMeditationReport(base.BaseTestCase):
self.old_stderr = None
self.CONF = self.useFixture(GmrConfigFixture(CONF)).conf
def test_basic_report(self):
report_lines = self.report.run().split('\n')
@ -168,6 +196,28 @@ class TestGuruMeditationReport(base.BaseTestCase):
os.kill(os.getpid(), signal.SIGUSR2)
self.assertIn('Guru Meditation', sys.stderr.getvalue())
@mock.patch.object(gmr.TextGuruMeditation, '_setup_file_watcher')
def test_register_autorun_without_signals(self, mock_setup_fh):
version = FakeVersionObj()
gmr.TextGuruMeditation.setup_autorun(version, conf=self.CONF)
mock_setup_fh.assert_called_once_with(
'/tmp/file', 10, version, None, '/var/fake_log')
@mock.patch('os.stat')
@mock.patch('time.sleep')
@mock.patch.object(threading.Thread, 'start')
def test_setup_file_watcher(self, mock_thread, mock_sleep, mock_stat):
version = FakeVersionObj()
mock_stat.return_value.st_mtime = 3
gmr.TextGuruMeditation._setup_file_watcher(
self.CONF.oslo_reports.file_event_handler,
self.CONF.oslo_reports.file_event_handler_interval,
version, None, self.CONF.oslo_reports.log_dir)
mock_stat.assert_called_once_with('/tmp/file')
self.assertEqual(1, mock_thread.called)
@mock.patch('oslo_utils.timeutils.utcnow',
return_value=datetime.datetime(2014, 1, 1, 12, 0, 0))
def test_register_autorun_log_dir(self, mock_strtime):