Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add common logging and notification.

This code is migrated from Nova, and will soon replace it.

Change-Id: I2dacac3ef251d419c7049154f6aaf0c18fdb9bb4
  • Loading branch information...
commit 16916b6129f075e54c1ead915d5131f6d34218ed 1 parent 2db4e68
Andrew Bogott authored
View
18 openstack/common/exception.py
@@ -19,6 +19,7 @@
Exceptions common to OpenStack projects
"""
+import itertools
import logging
@@ -145,3 +146,20 @@ class MalformedRequestBody(OpenstackException):
class InvalidContentType(OpenstackException):
message = "Invalid content type %(content_type)s"
+
+
+def get_context_from_function_and_args(function, args, kwargs):
+ """Find an arg of type RequestContext and return it.
+
+ This is useful in a couple of decorators where we don't
+ know much about the function we're wrapping.
+ """
+
+ # import here to avoid circularity:
+ from openstack.common import context
+
+ for arg in itertools.chain(kwargs.values(), args):
+ if isinstance(arg, context.RequestContext):
+ return arg
+
+ return None
View
456 openstack/common/log.py
@@ -0,0 +1,456 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+# 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.
+
+"""Openstack logging handler.
+
+This module adds to logging functionality by adding the option to specify
+a context object when calling the various log methods. If the context object
+is not specified, default formatting is used. Additionally, an instance uuid
+may be passed as part of the log message, which is intended to make it easier
+for admins to find messages related to a specific instance.
+
+It also allows setting of formatting information through conf.
+
+"""
+
+import cStringIO
+import inspect
+import itertools
+import json
+import logging
+import logging.config
+import logging.handlers
+import os
+import stat
+import sys
+import traceback
+
+from openstack.common import cfg
+from openstack.common import local
+from openstack.common import notifier
+
+
+log_opts = [
+ cfg.StrOpt('logging_context_format_string',
+ default='%(asctime)s %(levelname)s %(name)s [%(request_id)s '
+ '%(user_id)s %(project_id)s] %(instance)s'
+ '%(message)s',
+ help='format string to use for log messages with context'),
+ cfg.StrOpt('logging_default_format_string',
+ default='%(asctime)s %(levelname)s %(name)s [-] %(instance)s'
+ '%(message)s',
+ help='format string to use for log messages without context'),
+ cfg.StrOpt('logging_debug_format_suffix',
+ default='from (pid=%(process)d) %(funcName)s '
+ '%(pathname)s:%(lineno)d',
+ help='data to append to log format when level is DEBUG'),
+ cfg.StrOpt('logging_exception_prefix',
+ default='%(asctime)s TRACE %(name)s %(instance)s',
+ help='prefix each line of exception output with this format'),
+ cfg.ListOpt('default_log_levels',
+ default=[
+ 'amqplib=WARN',
+ 'sqlalchemy=WARN',
+ 'boto=WARN',
+ 'suds=INFO',
+ 'keystone=INFO',
+ 'eventlet.wsgi.server=WARN'
+ ],
+ help='list of logger=LEVEL pairs'),
+ cfg.BoolOpt('publish_errors',
+ default=False,
+ help='publish error events'),
+
+ # NOTE(mikal): there are two options here because sometimes we are handed
+ # a full instance (and could include more information), and other times we
+ # are just handed a UUID for the instance.
+ cfg.StrOpt('instance_format',
+ default='[instance: %(uuid)s] ',
+ help='If an instance is passed with the log message, format '
+ 'it like this'),
+ cfg.StrOpt('instance_uuid_format',
+ default='[instance: %(uuid)s] ',
+ help='If an instance UUID is passed with the log message, '
+ 'format it like this'),
+ ]
+
+
+generic_log_opts = [
+ cfg.StrOpt('logdir',
+ default=None,
+ help='Log output to a per-service log file in named directory'),
+ cfg.StrOpt('logfile',
+ default=None,
+ help='Log output to a named file'),
+ cfg.BoolOpt('use_stderr',
+ default=True,
+ help='Log output to standard error'),
+ cfg.StrOpt('logfile_mode',
+ default='0644',
+ help='Default file mode used when creating log files'),
+ ]
+
+
+CONF = cfg.CONF
+CONF.register_opts(generic_log_opts)
+CONF.register_opts(log_opts)
+
+# our new audit level
+# NOTE(jkoelker) Since we synthesized an audit level, make the logging
+# module aware of it so it acts like other levels.
+logging.AUDIT = logging.INFO + 1
+logging.addLevelName(logging.AUDIT, 'AUDIT')
+
+
+try:
+ NullHandler = logging.NullHandler
+except AttributeError: # NOTE(jkoelker) NullHandler added in Python 2.7
+ class NullHandler(logging.Handler):
+ def handle(self, record):
+ pass
+
+ def emit(self, record):
+ pass
+
+ def createLock(self):
+ self.lock = None
+
+
+def _dictify_context(context):
+ if context is None:
+ return None
+ if not isinstance(context, dict) and getattr(context, 'to_dict', None):
+ context = context.to_dict()
+ return context
+
+
+def _get_binary_name():
+ return os.path.basename(inspect.stack()[-1][1])
+
+
+def _get_log_file_path(binary=None):
+ logfile = CONF.log_file or CONF.logfile
+ logdir = CONF.log_dir or CONF.logdir
+
+ if logfile and not logdir:
+ return logfile
+
+ if logfile and logdir:
+ return os.path.join(logdir, logfile)
+
+ if logdir:
+ binary = binary or _get_binary_name()
+ return '%s.log' % (os.path.join(logdir, binary),)
+
+
+class ContextAdapter(logging.LoggerAdapter):
+ warn = logging.LoggerAdapter.warning
+
+ def __init__(self, logger, project_name, version_string):
+ self.logger = logger
+ self.project = project_name
+ self.version = version_string
+
+ def audit(self, msg, *args, **kwargs):
+ self.log(logging.AUDIT, msg, *args, **kwargs)
+
+ def process(self, msg, kwargs):
+ if 'extra' not in kwargs:
+ kwargs['extra'] = {}
+ extra = kwargs['extra']
+
+ context = kwargs.pop('context', None)
+ if not context:
+ context = getattr(local.store, 'context', None)
+ if context:
+ extra.update(_dictify_context(context))
+
+ instance = kwargs.pop('instance', None)
+ instance_extra = ''
+ if instance:
+ instance_extra = CONF.instance_format % instance
+ else:
+ instance_uuid = kwargs.pop('instance_uuid', None)
+ if instance_uuid:
+ instance_extra = (CONF.instance_uuid_format
+ % {'uuid': instance_uuid})
+ extra.update({'instance': instance_extra})
+
+ extra.update({"project": self.project})
+ extra.update({"version": self.version})
+ extra['extra'] = extra.copy()
+ return msg, kwargs
+
+
+class JSONFormatter(logging.Formatter):
+ def __init__(self, fmt=None, datefmt=None):
+ # NOTE(jkoelker) we ignore the fmt argument, but its still there
+ # since logging.config.fileConfig passes it.
+ self.datefmt = datefmt
+
+ def formatException(self, ei, strip_newlines=True):
+ lines = traceback.format_exception(*ei)
+ if strip_newlines:
+ lines = [itertools.ifilter(lambda x: x,
+ line.rstrip().splitlines())
+ for line in lines]
+ lines = list(itertools.chain(*lines))
+ return lines
+
+ def format(self, record):
+ message = {'message': record.getMessage(),
+ 'asctime': self.formatTime(record, self.datefmt),
+ 'name': record.name,
+ 'msg': record.msg,
+ 'args': record.args,
+ 'levelname': record.levelname,
+ 'levelno': record.levelno,
+ 'pathname': record.pathname,
+ 'filename': record.filename,
+ 'module': record.module,
+ 'lineno': record.lineno,
+ 'funcname': record.funcName,
+ 'created': record.created,
+ 'msecs': record.msecs,
+ 'relative_created': record.relativeCreated,
+ 'thread': record.thread,
+ 'thread_name': record.threadName,
+ 'process_name': record.processName,
+ 'process': record.process,
+ 'traceback': None}
+
+ if hasattr(record, 'extra'):
+ message['extra'] = record.extra
+
+ if record.exc_info:
+ message['traceback'] = self.formatException(record.exc_info)
+
+ return json.dumps(message)
+
+
+class PublishErrorsHandler(logging.Handler):
+ def emit(self, record):
+ if 'list_notifier_drivers' in CONF:
+ if ('openstack.common.notifier.log_notifier' in
+ CONF.list_notifier_drivers):
+ return
+ notifier.api.notify(None, 'error.publisher',
+ 'error_notification',
+ notifier.api.ERROR,
+ dict(error=record.msg))
+
+
+def handle_exception(type, value, tb):
+ extra = {}
+ if CONF.verbose:
+ extra['exc_info'] = (type, value, tb)
+ getLogger().critical(str(value), **extra)
+
+
+def setup(product_name):
+ """Setup logging."""
+ sys.excepthook = handle_exception
+
+ if CONF.log_config:
+ try:
+ logging.config.fileConfig(CONF.log_config)
+ except Exception:
+ traceback.print_exc()
+ raise
+ else:
+ _setup_logging_from_conf(product_name)
+
+
+def _find_facility_from_conf():
+ facility_names = logging.handlers.SysLogHandler.facility_names
+ facility = getattr(logging.handlers.SysLogHandler,
+ CONF.syslog_log_facility,
+ None)
+
+ if facility is None and CONF.syslog_log_facility in facility_names:
+ facility = facility_names.get(CONF.syslog_log_facility)
+
+ if facility is None:
+ valid_facilities = facility_names.keys()
+ consts = ['LOG_AUTH', 'LOG_AUTHPRIV', 'LOG_CRON', 'LOG_DAEMON',
+ 'LOG_FTP', 'LOG_KERN', 'LOG_LPR', 'LOG_MAIL', 'LOG_NEWS',
+ 'LOG_AUTH', 'LOG_SYSLOG', 'LOG_USER', 'LOG_UUCP',
+ 'LOG_LOCAL0', 'LOG_LOCAL1', 'LOG_LOCAL2', 'LOG_LOCAL3',
+ 'LOG_LOCAL4', 'LOG_LOCAL5', 'LOG_LOCAL6', 'LOG_LOCAL7']
+ valid_facilities.extend(consts)
+ raise TypeError(_('syslog facility must be one of: %s') %
+ ', '.join("'%s'" % fac
+ for fac in valid_facilities))
+
+ return facility
+
+
+def _setup_logging_from_conf(product_name):
+ log_root = getLogger(product_name).logger
+ for handler in log_root.handlers:
+ log_root.removeHandler(handler)
+
+ if CONF.use_syslog:
+ facility = _find_facility_from_conf()
+ syslog = logging.handlers.SysLogHandler(address='/dev/log',
+ facility=facility)
+ log_root.addHandler(syslog)
+
+ logpath = _get_log_file_path()
+ if logpath:
+ filelog = logging.handlers.WatchedFileHandler(logpath)
+ log_root.addHandler(filelog)
+
+ mode = int(CONF.logfile_mode, 8)
+ st = os.stat(logpath)
+ if st.st_mode != (stat.S_IFREG | mode):
+ os.chmod(logpath, mode)
+
+ if CONF.use_stderr:
+ streamlog = ColorHandler()
+ log_root.addHandler(streamlog)
+
+ elif not CONF.log_file:
+ streamlog = logging.StreamHandler(stream=sys.stdout)
+ log_root.addHandler(streamlog)
+
+ if CONF.publish_errors:
+ log_root.addHandler(PublishErrorsHandler(logging.ERROR))
+
+ for handler in log_root.handlers:
+ datefmt = CONF.log_date_format
+ if CONF.log_format:
+ handler.setFormatter(logging.Formatter(fmt=CONF.log_format,
+ datefmt=datefmt))
+ handler.setFormatter(LegacyFormatter(datefmt=datefmt))
+
+ if CONF.verbose or CONF.debug:
+ log_root.setLevel(logging.DEBUG)
+ else:
+ log_root.setLevel(logging.INFO)
+
+ level = logging.NOTSET
+ for pair in CONF.default_log_levels:
+ mod, _sep, level_name = pair.partition('=')
+ level = logging.getLevelName(level_name)
+ logger = logging.getLogger(mod)
+ logger.setLevel(level)
+ for handler in log_root.handlers:
+ logger.addHandler(handler)
+
+ # NOTE(jkoelker) Clear the handlers for the root logger that was setup
+ # by basicConfig in nova/__init__.py and install the
+ # NullHandler.
+ root = logging.getLogger()
+ for handler in root.handlers:
+ root.removeHandler(handler)
+ handler = NullHandler()
+ handler.setFormatter(logging.Formatter())
+ root.addHandler(handler)
+
+
+_loggers = {}
+
+
+def getLogger(name='unknown', version='unknown'):
+ if name not in _loggers:
+ _loggers[name] = ContextAdapter(logging.getLogger(name),
+ name,
+ version)
+ return _loggers[name]
+
+
+class WritableLogger(object):
+ """A thin wrapper that responds to `write` and logs."""
+
+ def __init__(self, logger, level=logging.INFO):
+ self.logger = logger
+ self.level = level
+
+ def write(self, msg):
+ self.logger.log(self.level, msg)
+
+
+class LegacyFormatter(logging.Formatter):
+ """A context.RequestContext aware formatter configured through flags.
+
+ The flags used to set format strings are: logging_context_format_string
+ and logging_default_format_string. You can also specify
+ logging_debug_format_suffix to append extra formatting if the log level is
+ debug.
+
+ For information about what variables are available for the formatter see:
+ http://docs.python.org/library/logging.html#formatter
+
+ """
+
+ def format(self, record):
+ """Uses contextstring if request_id is set, otherwise default."""
+ if 'instance' not in record.__dict__:
+ record.__dict__['instance'] = ''
+
+ if record.__dict__.get('request_id', None):
+ self._fmt = CONF.logging_context_format_string
+ else:
+ self._fmt = CONF.logging_default_format_string
+
+ if (record.levelno == logging.DEBUG and
+ CONF.logging_debug_format_suffix):
+ self._fmt += " " + CONF.logging_debug_format_suffix
+
+ # Cache this on the record, Logger will respect our formated copy
+ if record.exc_info:
+ record.exc_text = self.formatException(record.exc_info, record)
+ return logging.Formatter.format(self, record)
+
+ def formatException(self, exc_info, record=None):
+ """Format exception output with CONF.logging_exception_prefix."""
+ if not record:
+ return logging.Formatter.formatException(self, exc_info)
+
+ stringbuffer = cStringIO.StringIO()
+ traceback.print_exception(exc_info[0], exc_info[1], exc_info[2],
+ None, stringbuffer)
+ lines = stringbuffer.getvalue().split('\n')
+ stringbuffer.close()
+
+ if CONF.logging_exception_prefix.find('%(asctime)') != -1:
+ record.asctime = self.formatTime(record, self.datefmt)
+
+ formatted_lines = []
+ for line in lines:
+ pl = CONF.logging_exception_prefix % record.__dict__
+ fl = '%s%s' % (pl, line)
+ formatted_lines.append(fl)
+ return '\n'.join(formatted_lines)
+
+
+class ColorHandler(logging.StreamHandler):
+ LEVEL_COLORS = {
+ logging.DEBUG: '\033[00;32m', # GREEN
+ logging.INFO: '\033[00;36m', # CYAN
+ logging.AUDIT: '\033[01;36m', # BOLD CYAN
+ logging.WARN: '\033[01;33m', # BOLD YELLOW
+ logging.ERROR: '\033[01;31m', # BOLD RED
+ logging.CRITICAL: '\033[01;31m', # BOLD RED
+ }
+
+ def format(self, record):
+ record.color = self.LEVEL_COLORS[record.levelno]
+ return logging.StreamHandler.format(self, record)
View
14 openstack/common/notifier/__init__.py
@@ -0,0 +1,14 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# 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.
View
142 openstack/common/notifier/api.py
@@ -0,0 +1,142 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# 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.
+
+import inspect
+import uuid
+
+from openstack.common import cfg
+from openstack.common import exception
+from openstack.common import importutils
+from openstack.common import jsonutils
+from openstack.common import log as logging
+from openstack.common import timeutils
+from openstack.common import utils
+
+
+LOG = logging.getLogger(__name__)
+
+notifier_opts = [
+ cfg.StrOpt('notification_driver',
+ default='openstack.common.notifier.no_op_notifier',
+ help='Default driver for sending notifications'),
+ cfg.StrOpt('default_notification_level',
+ default='INFO',
+ help='Default notification level for outgoing notifications'),
+ cfg.StrOpt('default_publisher_id',
+ default='$host',
+ help='Default publisher_id for outgoing notifications'),
+ ]
+
+CONF = cfg.CONF
+CONF.register_opts(notifier_opts)
+
+WARN = 'WARN'
+INFO = 'INFO'
+ERROR = 'ERROR'
+CRITICAL = 'CRITICAL'
+DEBUG = 'DEBUG'
+
+log_levels = (DEBUG, WARN, INFO, ERROR, CRITICAL)
+
+
+class BadPriorityException(Exception):
+ pass
+
+
+def notify_decorator(name, fn):
+ """ decorator for notify which is used from utils.monkey_patch()
+
+ :param name: name of the function
+ :param function: - object of the function
+ :returns: function -- decorated function
+
+ """
+ def wrapped_func(*args, **kwarg):
+ body = {}
+ body['args'] = []
+ body['kwarg'] = {}
+ for arg in args:
+ body['args'].append(arg)
+ for key in kwarg:
+ body['kwarg'][key] = kwarg[key]
+
+ context = exception.get_context_from_function_and_args(fn, args, kwarg)
+ notify(context,
+ CONF.default_publisher_id,
+ name,
+ CONF.default_notification_level,
+ body)
+ return fn(*args, **kwarg)
+ return wrapped_func
+
+
+def publisher_id(service, host=None):
+ if not host:
+ host = CONF.host
+ return "%s.%s" % (service, host)
+
+
+def notify(context, publisher_id, event_type, priority, payload):
+ """Sends a notification using the specified driver
+
+ :param publisher_id: the source worker_type.host of the message
+ :param event_type: the literal type of event (ex. Instance Creation)
+ :param priority: patterned after the enumeration of Python logging
+ levels in the set (DEBUG, WARN, INFO, ERROR, CRITICAL)
+ :param payload: A python dictionary of attributes
+
+ Outgoing message format includes the above parameters, and appends the
+ following:
+
+ message_id
+ a UUID representing the id for this notification
+
+ timestamp
+ the GMT timestamp the notification was sent at
+
+ The composite message will be constructed as a dictionary of the above
+ attributes, which will then be sent via the transport mechanism defined
+ by the driver.
+
+ Message example::
+
+ {'message_id': str(uuid.uuid4()),
+ 'publisher_id': 'compute.host1',
+ 'timestamp': timeutils.utcnow(),
+ 'priority': 'WARN',
+ 'event_type': 'compute.create_instance',
+ 'payload': {'instance_id': 12, ... }}
+
+ """
+ if priority not in log_levels:
+ raise BadPriorityException(
+ _('%s not in valid priorities') % priority)
+
+ # Ensure everything is JSON serializable.
+ payload = jsonutils.to_primitive(payload, convert_instances=True)
+
+ driver = importutils.import_module(CONF.notification_driver)
+ msg = dict(message_id=str(uuid.uuid4()),
+ publisher_id=publisher_id,
+ event_type=event_type,
+ priority=priority,
+ payload=payload,
+ timestamp=str(timeutils.utcnow()))
+ try:
+ driver.notify(context, msg)
+ except Exception, e:
+ LOG.exception(_("Problem '%(e)s' attempting to "
+ "send to notification system. Payload=%(payload)s") %
+ locals())
View
116 openstack/common/notifier/list_notifier.py
@@ -0,0 +1,116 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# 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.
+
+from openstack.common import cfg
+from openstack.common import importutils
+from openstack.common import log as logging
+
+
+list_notifier_drivers_opt = cfg.MultiStrOpt('list_notifier_drivers',
+ default=['openstack.common.notifier.no_op_notifier'],
+ help='List of drivers to send notifications')
+
+CONF = cfg.CONF
+CONF.register_opt(list_notifier_drivers_opt)
+
+LOG = logging.getLogger(__name__)
+
+drivers = None
+
+
+class ImportFailureNotifier(object):
+ """Noisily re-raises some exception over-and-over when notify is called."""
+
+ def __init__(self, exception):
+ self.exception = exception
+
+ def notify(self, context, message):
+ raise self.exception
+
+
+def _get_drivers():
+ """Instantiates and returns drivers based on the flag values."""
+ global drivers
+ if drivers is None:
+ drivers = []
+ for notification_driver in CONF.list_notifier_drivers:
+ try:
+ drivers.append(importutils.import_module(notification_driver))
+ except ImportError as e:
+ drivers.append(ImportFailureNotifier(e))
+ return drivers
+
+
+def add_driver(notification_driver):
+ """Add a notification driver at runtime."""
+ # Make sure the driver list is initialized.
+ _get_drivers()
+ if isinstance(notification_driver, basestring):
+ # Load and add
+ try:
+ drivers.append(importutils.import_module(notification_driver))
+ except ImportError as e:
+ drivers.append(ImportFailureNotifier(e))
+ else:
+ # Driver is already loaded; just add the object.
+ drivers.append(notification_driver)
+
+
+def _object_name(obj):
+ name = []
+ if hasattr(obj, '__module__'):
+ name.append(obj.__module__)
+ if hasattr(obj, '__name__'):
+ name.append(obj.__name__)
+ else:
+ name.append(obj.__class__.__name__)
+ return '.'.join(name)
+
+
+def remove_driver(notification_driver):
+ """Remove a notification driver at runtime."""
+ # Make sure the driver list is initialized.
+ _get_drivers()
+ removed = False
+ if notification_driver in drivers:
+ # We're removing an object. Easy.
+ drivers.remove(notification_driver)
+ removed = True
+ else:
+ # We're removing a driver by name. Search for it.
+ for driver in drivers:
+ if _object_name(driver) == notification_driver:
+ drivers.remove(driver)
+ removed = True
+
+ if not removed:
+ raise ValueError("Cannot remove; %s is not in list" %
+ notification_driver)
+
+
+def notify(context, message):
+ """Passes notification to multiple notifiers in a list."""
+ for driver in _get_drivers():
+ try:
+ driver.notify(context, message)
+ except Exception as e:
+ LOG.exception(_("Problem '%(e)s' attempting to send to "
+ "notification driver %(driver)s."), locals())
+
+
+def _reset_drivers():
+ """Used by unit tests to reset the drivers."""
+ global drivers
+ drivers = None
View
34 openstack/common/notifier/log_notifier.py
@@ -0,0 +1,34 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# 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.
+
+import json
+
+from openstack.common import cfg
+from openstack.common import log as logging
+
+
+CONF = cfg.CONF
+
+
+def notify(_context, message):
+ """Notifies the recipient of the desired event given the model.
+ Log notifications using openstack's default logging system"""
+
+ priority = message.get('priority',
+ CONF.default_notification_level)
+ priority = priority.lower()
+ logger = logging.getLogger(
+ 'openstack.common.notification.%s' % message['event_type'])
+ getattr(logger, priority)(json.dumps(message))
View
19 openstack/common/notifier/no_op_notifier.py
@@ -0,0 +1,19 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# 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.
+
+
+def notify(_context, message):
+ """Notifies the recipient of the desired event given the model"""
+ pass
View
45 openstack/common/notifier/rabbit_notifier.py
@@ -0,0 +1,45 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# 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.
+
+
+from openstack.common import cfg
+from openstack.common import context as req_context
+from openstack.common import log as logging
+from openstack.common import rpc
+
+LOG = logging.getLogger(__name__)
+
+notification_topic_opt = cfg.ListOpt('notification_topics',
+ default=['notifications', ],
+ help='AMQP topic used for openstack notifications')
+
+CONF = cfg.CONF
+CONF.register_opt(notification_topic_opt)
+
+
+def notify(context, message):
+ """Sends a notification to the RabbitMQ"""
+ if not context:
+ context = req_context.get_admin_context()
+ priority = message.get('priority',
+ CONF.default_notification_level)
+ priority = priority.lower()
+ for topic in CONF.notification_topics:
+ topic = '%s.%s' % (topic, priority)
+ try:
+ rpc.notify(context, topic, message)
+ except Exception, e:
+ LOG.exception(_("Could not send notification to %(topic)s. "
+ "Payload=%(message)s"), locals())
View
22 openstack/common/notifier/test_notifier.py
@@ -0,0 +1,22 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# 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.
+
+
+NOTIFICATIONS = []
+
+
+def notify(_context, message):
+ """Test notifier, stores notifications in memory for unittests."""
+ NOTIFICATIONS.append(message)
View
14 tests/unit/notifier/__init__.py
@@ -0,0 +1,14 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# 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.
View
135 tests/unit/notifier/test_list_notifier.py
@@ -0,0 +1,135 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# 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.
+
+from openstack.common import log as logging
+from openstack.common.notifier import api
+from openstack.common.notifier import list_notifier
+from openstack.common.notifier import log_notifier
+from openstack.common.notifier import no_op_notifier
+from tests import utils as test_utils
+
+
+class SimpleNotifier(object):
+ def __init__(self):
+ self.notified = False
+
+ def notify(self, *args):
+ self.notified = True
+
+
+class NotifierListTestCase(test_utils.BaseTestCase):
+ """Test case for notifications"""
+
+ def setUp(self):
+ super(NotifierListTestCase, self).setUp()
+ list_notifier._reset_drivers()
+ # Mock log to add one to exception_count when log.exception is called
+
+ def mock_exception(cls, *args):
+ self.exception_count += 1
+
+ self.exception_count = 0
+ list_notifier_log = logging.getLogger(
+ 'openstack.common.notifier.list_notifier')
+ self.stubs.Set(list_notifier_log, "exception", mock_exception)
+ # Mock no_op notifier to add one to notify_count when called.
+
+ def mock_notify(cls, *args):
+ self.notify_count += 1
+
+ self.notify_count = 0
+ self.stubs.Set(no_op_notifier, 'notify', mock_notify)
+ # Mock log_notifier to raise RuntimeError when called.
+
+ def mock_notify2(cls, *args):
+ raise RuntimeError("Bad notifier.")
+
+ self.stubs.Set(log_notifier, 'notify', mock_notify2)
+
+ def tearDown(self):
+ list_notifier._reset_drivers()
+ super(NotifierListTestCase, self).tearDown()
+
+ def test_send_notifications_successfully(self):
+ self.config(notification_driver='openstack.common.'
+ 'notifier.list_notifier',
+ list_notifier_drivers=[
+ 'openstack.common.notifier.no_op_notifier',
+ 'openstack.common.notifier.no_op_notifier'])
+ api.notify('contextarg', 'publisher_id', 'event_type',
+ api.WARN, dict(a=3))
+ self.assertEqual(self.notify_count, 2)
+ self.assertEqual(self.exception_count, 0)
+
+ def test_send_notifications_with_errors(self):
+
+ self.config(notification_driver='openstack.common.'
+ 'notifier.list_notifier',
+ list_notifier_drivers=[
+ 'openstack.common.notifier.no_op_notifier',
+ 'openstack.common.notifier.log_notifier'])
+ api.notify('contextarg', 'publisher_id',
+ 'event_type', api.WARN, dict(a=3))
+ self.assertEqual(self.notify_count, 1)
+ self.assertEqual(self.exception_count, 1)
+
+ def test_when_driver_fails_to_import(self):
+ self.config(notification_driver='openstack.common.'
+ 'notifier.list_notifier',
+ list_notifier_drivers=[
+ 'openstack.common.notifier.no_op_notifier',
+ 'openstack.common.notifier.logo_notifier',
+ 'fdsjgsdfhjkhgsfkj'])
+ api.notify('contextarg', 'publisher_id',
+ 'event_type', api.WARN, dict(a=3))
+ self.assertEqual(self.exception_count, 2)
+ self.assertEqual(self.notify_count, 1)
+
+ def test_adding_and_removing_notifier_object(self):
+ self.notifier_object = SimpleNotifier()
+ self.config(notification_driver='openstack.common.'
+ 'notifier.list_notifier',
+ list_notifier_drivers=[
+ 'openstack.common.notifier.no_op_notifier'])
+
+ list_notifier.add_driver(self.notifier_object)
+ api.notify(None, 'publisher_id', 'event_type',
+ api.WARN, dict(a=3))
+ self.assertEqual(self.notify_count, 1)
+ self.assertTrue(self.notifier_object.notified)
+
+ self.notifier_object.notified = False
+ list_notifier.remove_driver(self.notifier_object)
+
+ api.notify(None, 'publisher_id', 'event_type',
+ api.WARN, dict(a=3))
+ self.assertEqual(self.notify_count, 2)
+ self.assertFalse(self.notifier_object.notified)
+
+ def test_adding_and_removing_notifier_module(self):
+ self.config(notification_driver='openstack.common.'
+ 'notifier.list_notifier',
+ list_notifier_drivers=[])
+
+ list_notifier.add_driver('openstack.common.notifier.no_op_notifier')
+ api.notify(None, 'publisher_id', 'event_type',
+ api.WARN, dict(a=3))
+ self.assertEqual(self.notify_count, 1)
+
+ list_notifier.remove_driver('openstack.common.notifier.no_op_notifier')
+
+ api.notify(None, 'publisher_id', 'event_type',
+ api.WARN, dict(a=3))
+ self.assertEqual(self.notify_count, 1)
View
218 tests/unit/test_log.py
@@ -0,0 +1,218 @@
+import cStringIO
+import json
+import logging
+import sys
+
+from openstack.common import context
+from openstack.common import cfg
+from openstack.common import log
+from openstack.common.notifier import api as notifier
+from openstack.common.notifier import list_notifier
+from tests import utils as test_utils
+
+CONF = cfg.CONF
+
+
+def _fake_context():
+ return context.RequestContext(1, 1)
+
+
+class LoggerTestCase(test_utils.BaseTestCase):
+ def setUp(self):
+ super(LoggerTestCase, self).setUp()
+ self.log = log.getLogger()
+
+ def test_handlers_have_legacy_formatter(self):
+ formatters = []
+ for h in self.log.logger.handlers:
+ f = h.formatter
+ if isinstance(f, log.LegacyFormatter):
+ formatters.append(f)
+ self.assert_(formatters)
+ self.assertEqual(len(formatters), len(self.log.logger.handlers))
+
+ def test_handles_context_kwarg(self):
+ self.log.info("foo", context=_fake_context())
+ self.assert_(True) # didn't raise exception
+
+ def test_audit_handles_context_arg(self):
+ self.log.audit("foo", context=_fake_context())
+ self.assert_(True) # didn't raise exception
+
+ def test_will_be_verbose_if_verbose_flag_set(self):
+ self.config(verbose=True)
+ log.setup()
+ self.assertEqual(logging.DEBUG, self.log.logger.getEffectiveLevel())
+
+ def test_will_not_be_verbose_if_verbose_flag_not_set(self):
+ self.config(verbose=False)
+ log.setup()
+ self.assertEqual(logging.INFO, self.log.logger.getEffectiveLevel())
+
+ def test_no_logging_via_module(self):
+ for func in ('critical', 'error', 'exception', 'warning', 'warn',
+ 'info', 'debug', 'log', 'audit'):
+ self.assertRaises(AttributeError, getattr, log, func)
+
+
+class LogHandlerTestCase(test_utils.BaseTestCase):
+ def test_log_path_logdir(self):
+ self.config(logdir='/some/path', logfile=None)
+ self.assertEquals(log._get_log_file_path(binary='foo-bar'),
+ '/some/path/foo-bar.log')
+
+ def test_log_path_logfile(self):
+ self.config(logfile='/some/path/foo-bar.log')
+ self.assertEquals(log._get_log_file_path(binary='foo-bar'),
+ '/some/path/foo-bar.log')
+
+ def test_log_path_none(self):
+ self.config(logdir=None, logfile=None)
+ self.assertTrue(log._get_log_file_path(binary='foo-bar') is None)
+
+ def test_log_path_logfile_overrides_logdir(self):
+ self.config(logdir='/some/other/path',
+ logfile='/some/path/foo-bar.log')
+ self.assertEquals(log._get_log_file_path(binary='foo-bar'),
+ '/some/path/foo-bar.log')
+
+
+class PublishErrorsHandlerTestCase(test_utils.BaseTestCase):
+ """Tests for log.PublishErrorsHandler"""
+ def setUp(self):
+ super(PublishErrorsHandlerTestCase, self).setUp()
+ self.publiserrorshandler = log.PublishErrorsHandler(logging.ERROR)
+
+ def test_emit_cfg_list_notifier_drivers_in_flags(self):
+ self.stub_flg = False
+
+ def fake_notifier(*args, **kwargs):
+ self.stub_flg = True
+
+ self.stubs.Set(notifier, 'notify', fake_notifier)
+ logrecord = logging.LogRecord('name', 'WARN', '/tmp', 1,
+ 'Message', None, None)
+ self.publiserrorshandler.emit(logrecord)
+ self.assertTrue(self.stub_flg)
+
+ def test_emit_cfg_log_notifier_in_list_notifier_drivers(self):
+ self.config(list_notifier_drivers=[
+ 'openstack.common.notifier.rabbit_notifier',
+ 'openstack.common.notifier.log_notifier'])
+ self.stub_flg = True
+
+ def fake_notifier(*args, **kwargs):
+ self.stub_flg = False
+
+ self.stubs.Set(notifier, 'notify', fake_notifier)
+ logrecord = logging.LogRecord('name', 'WARN', '/tmp', 1,
+ 'Message', None, None)
+ self.publiserrorshandler.emit(logrecord)
+ self.assertTrue(self.stub_flg)
+
+
+class LoggerTestCase(test_utils.BaseTestCase):
+ def setUp(self):
+ super(LoggerTestCase, self).setUp()
+ levels = CONF.default_log_levels
+ levels.append("nova-test=AUDIT")
+ self.config(default_log_levels=levels,
+ verbose=True)
+ log.setup('testing')
+ self.log = log.getLogger('nova-test')
+
+ def test_has_level_from_flags(self):
+ self.assertEqual(logging.AUDIT, self.log.logger.getEffectiveLevel())
+
+ def test_child_log_has_level_of_parent_flag(self):
+ l = log.getLogger('nova-test.foo')
+ self.assertEqual(logging.AUDIT, l.logger.getEffectiveLevel())
+
+
+class JSONFormatterTestCase(test_utils.BaseTestCase):
+ def setUp(self):
+ super(JSONFormatterTestCase, self).setUp()
+ self.log = log.getLogger('test-json')
+ self.stream = cStringIO.StringIO()
+ handler = logging.StreamHandler(self.stream)
+ handler.setFormatter(log.JSONFormatter())
+ self.log.logger.addHandler(handler)
+ self.log.logger.setLevel(logging.DEBUG)
+
+ def test_json(self):
+ test_msg = 'This is a %(test)s line'
+ test_data = {'test': 'log'}
+ self.log.debug(test_msg, test_data)
+
+ data = json.loads(self.stream.getvalue())
+ self.assertTrue(data)
+ self.assertTrue('extra' in data)
+ self.assertEqual('test-json', data['name'])
+
+ self.assertEqual(test_msg % test_data, data['message'])
+ self.assertEqual(test_msg, data['msg'])
+ self.assertEqual(test_data, data['args'])
+
+ self.assertEqual('test_log.py', data['filename'])
+ self.assertEqual('test_json', data['funcname'])
+
+ self.assertEqual('DEBUG', data['levelname'])
+ self.assertEqual(logging.DEBUG, data['levelno'])
+ self.assertFalse(data['traceback'])
+
+ def test_json_exception(self):
+ test_msg = 'This is %s'
+ test_data = 'exceptional'
+ try:
+ raise Exception('This is exceptional')
+ except Exception:
+ self.log.exception(test_msg, test_data)
+
+ data = json.loads(self.stream.getvalue())
+ self.assertTrue(data)
+ self.assertTrue('extra' in data)
+ self.assertEqual('test-json', data['name'])
+
+ self.assertEqual(test_msg % test_data, data['message'])
+ self.assertEqual(test_msg, data['msg'])
+ self.assertEqual([test_data], data['args'])
+
+ self.assertEqual('ERROR', data['levelname'])
+ self.assertEqual(logging.ERROR, data['levelno'])
+ self.assertTrue(data['traceback'])
+
+
+class LegacyFormatterTestCase(test_utils.BaseTestCase):
+ def setUp(self):
+ super(LegacyFormatterTestCase, self).setUp()
+ self.config(logging_context_format_string="HAS CONTEXT "
+ "[%(request_id)s]: "
+ "%(message)s",
+ logging_default_format_string="NOCTXT: %(message)s",
+ logging_debug_format_suffix="--DBG")
+ self.log = log.getLogger()
+ self.stream = cStringIO.StringIO()
+ self.handler = logging.StreamHandler(self.stream)
+ self.handler.setFormatter(log.LegacyFormatter())
+ self.log.logger.addHandler(self.handler)
+ self.level = self.log.logger.getEffectiveLevel()
+ self.log.logger.setLevel(logging.DEBUG)
+
+ def tearDown(self):
+ self.log.logger.setLevel(self.level)
+ self.log.logger.removeHandler(self.handler)
+ super(LegacyFormatterTestCase, self).tearDown()
+
+ def test_uncontextualized_log(self):
+ self.log.info("foo")
+ self.assertEqual("NOCTXT: foo\n", self.stream.getvalue())
+
+ def test_contextualized_log(self):
+ ctxt = _fake_context()
+ self.log.info("bar", context=ctxt)
+ expected = "HAS CONTEXT [%s]: bar\n" % ctxt.request_id
+ self.assertEqual(expected, self.stream.getvalue())
+
+ def test_debugging_log(self):
+ self.log.debug("baz")
+ self.assertEqual("NOCTXT: baz --DBG\n", self.stream.getvalue())
View
186 tests/unit/test_notifier.py
@@ -0,0 +1,186 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# 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.
+
+from openstack.common import cfg
+from openstack.common import context
+from openstack.common import log
+from openstack.common.notifier import api as notifier_api
+from openstack.common.notifier import no_op_notifier
+from openstack.common.notifier import rabbit_notifier
+from openstack.common import rpc
+from tests import utils as test_utils
+
+
+ctxt = context.get_admin_context()
+ctxt2 = context.get_admin_context()
+
+
+class NotifierTestCase(test_utils.BaseTestCase):
+ """Test case for notifications"""
+ def setUp(self):
+ super(NotifierTestCase, self).setUp()
+ self.config(notification_driver='openstack.common.'
+ 'notifier.no_op_notifier')
+ self.config(default_publisher_id='publisher')
+
+ def test_send_notification(self):
+ self.notify_called = False
+
+ def mock_notify(cls, *args):
+ self.notify_called = True
+
+ self.stubs.Set(no_op_notifier, 'notify',
+ mock_notify)
+
+ notifier_api.notify(ctxt, 'publisher_id', 'event_type',
+ notifier_api.WARN, dict(a=3))
+ self.assertEqual(self.notify_called, True)
+
+ def test_verify_message_format(self):
+ """A test to ensure changing the message format is prohibitively
+ annoying"""
+
+ def message_assert(context, message):
+ fields = [('publisher_id', 'publisher_id'),
+ ('event_type', 'event_type'),
+ ('priority', 'WARN'),
+ ('payload', dict(a=3))]
+ for k, v in fields:
+ self.assertEqual(message[k], v)
+ self.assertTrue(len(message['message_id']) > 0)
+ self.assertTrue(len(message['timestamp']) > 0)
+ self.assertEqual(context, ctxt)
+
+ self.stubs.Set(no_op_notifier, 'notify',
+ message_assert)
+ notifier_api.notify(ctxt, 'publisher_id', 'event_type',
+ notifier_api.WARN, dict(a=3))
+
+ def test_send_rabbit_notification(self):
+ self.stubs.Set(cfg.CONF, 'notification_driver',
+ 'openstack.common.notifier.rabbit_notifier')
+ self.mock_notify = False
+
+ def mock_notify(cls, *args):
+ self.mock_notify = True
+
+ self.stubs.Set(rpc, 'notify', mock_notify)
+ notifier_api.notify(ctxt, 'publisher_id', 'event_type',
+ notifier_api.WARN, dict(a=3))
+
+ self.assertEqual(self.mock_notify, True)
+
+ def test_invalid_priority(self):
+ self.assertRaises(notifier_api.BadPriorityException,
+ notifier_api.notify, ctxt, 'publisher_id',
+ 'event_type', 'not a priority', dict(a=3))
+
+ def test_rabbit_priority_queue(self):
+ self.stubs.Set(cfg.CONF, 'notification_driver',
+ 'openstack.common.notifier.rabbit_notifier')
+ self.stubs.Set(cfg.CONF, 'notification_topics',
+ ['testnotify', ])
+
+ self.test_topic = None
+
+ def mock_notify(context, topic, msg):
+ self.test_topic = topic
+
+ self.stubs.Set(rpc, 'notify', mock_notify)
+ notifier_api.notify(ctxt, 'publisher_id',
+ 'event_type', 'DEBUG', dict(a=3))
+ self.assertEqual(self.test_topic, 'testnotify.debug')
+
+ def test_error_notification(self):
+ self.stubs.Set(cfg.CONF, 'notification_driver',
+ 'openstack.common.notifier.rabbit_notifier')
+ self.stubs.Set(cfg.CONF, 'publish_errors', True)
+ LOG = log.getLogger('common')
+ log.setup(None)
+ msgs = []
+
+ def mock_notify(context, topic, data):
+ msgs.append(data)
+
+ self.stubs.Set(rpc, 'notify', mock_notify)
+ LOG.error('foo')
+ self.assertEqual(1, len(msgs))
+ msg = msgs[0]
+ self.assertEqual(msg['event_type'], 'error_notification')
+ self.assertEqual(msg['priority'], 'ERROR')
+ self.assertEqual(msg['payload']['error'], 'foo')
+
+ def test_send_notification_by_decorator(self):
+ self.notify_called = False
+
+ def example_api(arg1, arg2):
+ return arg1 + arg2
+
+ example_api = notifier_api.notify_decorator(
+ 'example_api',
+ example_api)
+
+ def mock_notify(cls, *args):
+ self.notify_called = True
+
+ self.stubs.Set(no_op_notifier, 'notify',
+ mock_notify)
+
+ self.assertEqual(3, example_api(1, 2))
+ self.assertEqual(self.notify_called, True)
+
+ def test_decorator_context(self):
+ """Verify that the notify decorator can extract the 'context' arg."""
+ self.notify_called = False
+ self.context_arg = None
+
+ def example_api(arg1, arg2, context):
+ return arg1 + arg2
+
+ def example_api2(arg1, arg2, **kw):
+ return arg1 + arg2
+
+ example_api = notifier_api.notify_decorator(
+ 'example_api',
+ example_api)
+
+ example_api2 = notifier_api.notify_decorator(
+ 'example_api2',
+ example_api2)
+
+ def mock_notify(context, cls, _type, _priority, _payload):
+ self.notify_called = True
+ self.context_arg = context
+
+ self.stubs.Set(notifier_api, 'notify',
+ mock_notify)
+
+ # Test positional context
+ self.assertEqual(3, example_api(1, 2, ctxt))
+ self.assertEqual(self.notify_called, True)
+ self.assertEqual(self.context_arg, ctxt)
+
+ self.notify_called = False
+ self.context_arg = None
+
+ # Test named context
+ self.assertEqual(3, example_api2(1, 2, context=ctxt2))
+ self.assertEqual(self.notify_called, True)
+ self.assertEqual(self.context_arg, ctxt2)
+
+ # Test missing context
+ self.assertEqual(3, example_api2(1, 2, bananas="delicious"))
+ self.assertEqual(self.notify_called, True)
+ self.assertEqual(self.context_arg, None)
Please sign in to comment.
Something went wrong with that request. Please try again.