diff --git a/oslo_reports/guru_meditation_report.py b/oslo_reports/guru_meditation_report.py index a1c62ef..3ff497d 100644 --- a/oslo_reports/guru_meditation_report.py +++ b/oslo_reports/guru_meditation_report.py @@ -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): diff --git a/oslo_reports/opts.py b/oslo_reports/opts.py index 94072bf..324b7f1 100644 --- a/oslo_reports/opts.py +++ b/oslo_reports/opts.py @@ -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')) ] diff --git a/oslo_reports/tests/test_guru_meditation_report.py b/oslo_reports/tests/test_guru_meditation_report.py index b83f536..b86bcf3 100644 --- a/oslo_reports/tests/test_guru_meditation_report.py +++ b/oslo_reports/tests/test_guru_meditation_report.py @@ -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):