oslo.reports/oslo_reports/guru_meditation_report.py
Ivan Kolodyazhny d23e0a65b2 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
2016-06-13 18:25:14 +03:00

300 lines
10 KiB
Python

# Copyright 2013 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Provides Guru Meditation Report
This module defines the actual OpenStack Guru Meditation
Report class.
This can be used in the OpenStack command definition files.
For example, in a nova command module (under nova/cmd):
.. code-block:: python
:emphasize-lines: 8,9,10
from oslo_config import cfg
from oslo_log import log as oslo_logging
from oslo_reports import opts as gmr_opts
from oslo_reports import guru_meditation_report as gmr
CONF = cfg.CONF
# maybe import some options here...
def main():
oslo_logging.register_options(CONF)
gmr_opts.set_defaults(CONF)
CONF(sys.argv[1:], default_config_files=['myapp.conf'])
oslo_logging.setup(CONF, 'myapp')
gmr.TextGuruMeditation.register_section('Some Special Section',
special_section_generator)
gmr.TextGuruMeditation.setup_autorun(version_object, conf=CONF)
server = service.Service.create(binary='some-service',
topic=CONF.some_service_topic)
service.serve(server)
service.wait()
Then, you can do
.. code-block:: bash
$ kill -USR2 $SERVICE_PID
and get a Guru Meditation Report in the file or terminal
where stderr is logged for that given service.
"""
from __future__ import print_function
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
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__)
class GuruMeditation(object):
"""A Guru Meditation Report Mixin/Base Class
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 or use file modification events.
This class should always be used in conjunction with
a Report class via multiple inheritance. It should
always come first in the class list to ensure the
MRO is correct.
"""
timestamp_fmt = "%Y%m%d%H%M%S"
def __init__(self, version_obj, sig_handler_tb=None, *args, **kwargs):
self.version_obj = version_obj
self.traceback = sig_handler_tb
super(GuruMeditation, self).__init__(*args, **kwargs)
self.start_section_index = len(self.sections)
@classmethod
def register_section(cls, section_title, generator):
"""Register a New Section
This method registers a persistent section for the current
class.
:param str section_title: the title of the section
:param generator: the generator for the section
"""
try:
cls.persistent_sections.append([section_title, generator])
except AttributeError:
cls.persistent_sections = [[section_title, generator]]
@classmethod
def setup_autorun(cls, version, service_name=None,
log_dir=None, signum=None, conf=None):
"""Set Up Auto-Run
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. 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
:param logdir: path to a log directory where to create a file
:param signum: the signal to associate with running the report
:param conf: Configuration object, managed by the caller.
"""
if log_dir is None and conf is not None:
log_dir = conf.oslo_reports.log_dir
if signum:
cls._setup_signal(signum, version, service_name, log_dir)
return
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):
signal.signal(signum,
lambda sn, f: cls.handle_signal(
version, service_name, log_dir, f))
@classmethod
def handle_signal(cls, version, service_name, log_dir, frame):
"""The Signal Handler
This method (indirectly) handles receiving a registered signal and
dumping the Guru Meditation Report to stderr or a file in a given dir.
If service name and log dir are not None, the report will be dumped to
a file named $service_name_gurumeditation_$current_time in the log_dir
directory.
This method is designed to be curried into a proper signal handler by
currying out the version
parameter.
:param version: the version object for the current product
:param service_name: this program name used to construct logfile name
:param logdir: path to a log directory where to create a file
:param frame: the frame object provided to the signal handler
"""
try:
res = cls(version, frame).run()
except Exception:
traceback.print_exc(file=sys.stderr)
print("Unable to run Guru Meditation Report!",
file=sys.stderr)
else:
if log_dir:
service_name = service_name or os.path.basename(
inspect.stack()[-1][1])
filename = "%s_gurumeditation_%s" % (
service_name, timeutils.utcnow().strftime(
cls.timestamp_fmt))
filepath = os.path.join(log_dir, filename)
try:
with open(filepath, "w") as dumpfile:
dumpfile.write(res)
except Exception:
print("Unable to dump Guru Meditation Report to file %s" %
(filepath,), file=sys.stderr)
else:
print(res, file=sys.stderr)
def _readd_sections(self):
del self.sections[self.start_section_index:]
self.add_section('Package',
pgen.PackageReportGenerator(self.version_obj))
self.add_section('Threads',
tgen.ThreadReportGenerator(self.traceback))
self.add_section('Green Threads',
tgen.GreenThreadReportGenerator())
self.add_section('Processes',
prgen.ProcessReportGenerator())
self.add_section('Configuration',
cgen.ConfigReportGenerator())
try:
for section_title, generator in self.persistent_sections:
self.add_section(section_title, generator)
except AttributeError:
pass
def run(self):
self._readd_sections()
return super(GuruMeditation, self).run()
# GuruMeditation must come first to get the correct MRO
class TextGuruMeditation(GuruMeditation, report.TextReport):
"""A Text Guru Meditation Report
This report is the basic human-readable Guru Meditation Report
It contains the following sections by default
(in addition to any registered persistent sections):
- Package Information
- Threads List
- Green Threads List
- Process List
- Configuration Options
:param version_obj: the version object for the current product
:param traceback: an (optional) frame object providing the actual
traceback for the current thread
"""
def __init__(self, version_obj, traceback=None):
super(TextGuruMeditation, self).__init__(version_obj, traceback,
'Guru Meditation')