Import notifier middleware from oslo-incubator
Blueprint: graduate-notifier-middleware Change-Id: I2aa341f3e96a7344b87160609fb47e43bae5b245
This commit is contained in:
parent
fbaa46e276
commit
f8ea1a0f76
128
oslo/messaging/notify/middleware.py
Normal file
128
oslo/messaging/notify/middleware.py
Normal file
@ -0,0 +1,128 @@
|
||||
# Copyright (c) 2013-2014 eNovance
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Send notifications on request
|
||||
|
||||
"""
|
||||
import logging
|
||||
import os.path
|
||||
import sys
|
||||
import traceback as tb
|
||||
|
||||
import six
|
||||
import webob.dec
|
||||
|
||||
from oslo.config import cfg
|
||||
from oslo import messaging
|
||||
from oslo.messaging import notify
|
||||
from oslo.messaging.openstack.common import context
|
||||
from oslo.messaging.openstack.common.gettextutils import _LE
|
||||
from oslo.messaging.openstack.common.middleware import base
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def log_and_ignore_error(fn):
|
||||
def wrapped(*args, **kwargs):
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except Exception as e:
|
||||
LOG.exception(_LE('An exception occurred processing '
|
||||
'the API call: %s ') % e)
|
||||
return wrapped
|
||||
|
||||
|
||||
class RequestNotifier(base.Middleware):
|
||||
"""Send notification on request."""
|
||||
|
||||
@classmethod
|
||||
def factory(cls, global_conf, **local_conf):
|
||||
"""Factory method for paste.deploy."""
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
|
||||
def _factory(app):
|
||||
return cls(app, **conf)
|
||||
return _factory
|
||||
|
||||
def __init__(self, app, **conf):
|
||||
self.notifier = notify.Notifier(
|
||||
messaging.get_transport(cfg.CONF, conf.get('url')),
|
||||
publisher_id=conf.get('publisher_id',
|
||||
os.path.basename(sys.argv[0])))
|
||||
self.service_name = conf.get('service_name')
|
||||
self.ignore_req_list = [x.upper().strip() for x in
|
||||
conf.get('ignore_req_list', '').split(',')]
|
||||
super(RequestNotifier, self).__init__(app)
|
||||
|
||||
@staticmethod
|
||||
def environ_to_dict(environ):
|
||||
"""Following PEP 333, server variables are lower case, so don't
|
||||
include them.
|
||||
|
||||
"""
|
||||
return dict((k, v) for k, v in six.iteritems(environ)
|
||||
if k.isupper() and k != 'HTTP_X_AUTH_TOKEN')
|
||||
|
||||
@log_and_ignore_error
|
||||
def process_request(self, request):
|
||||
request.environ['HTTP_X_SERVICE_NAME'] = \
|
||||
self.service_name or request.host
|
||||
payload = {
|
||||
'request': self.environ_to_dict(request.environ),
|
||||
}
|
||||
|
||||
self.notifier.info(context.get_admin_context(),
|
||||
'http.request',
|
||||
payload)
|
||||
|
||||
@log_and_ignore_error
|
||||
def process_response(self, request, response,
|
||||
exception=None, traceback=None):
|
||||
payload = {
|
||||
'request': self.environ_to_dict(request.environ),
|
||||
}
|
||||
|
||||
if response:
|
||||
payload['response'] = {
|
||||
'status': response.status,
|
||||
'headers': response.headers,
|
||||
}
|
||||
|
||||
if exception:
|
||||
payload['exception'] = {
|
||||
'value': repr(exception),
|
||||
'traceback': tb.format_tb(traceback)
|
||||
}
|
||||
|
||||
self.notifier.info(context.get_admin_context(),
|
||||
'http.response',
|
||||
payload)
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
if req.method in self.ignore_req_list:
|
||||
return req.get_response(self.application)
|
||||
else:
|
||||
self.process_request(req)
|
||||
try:
|
||||
response = req.get_response(self.application)
|
||||
except Exception:
|
||||
exc_type, value, traceback = sys.exc_info()
|
||||
self.process_response(req, None, value, traceback)
|
||||
raise
|
||||
else:
|
||||
self.process_response(req, response)
|
||||
return response
|
@ -22,3 +22,6 @@ PyYAML>=3.1.0
|
||||
|
||||
# rabbit driver is the default
|
||||
kombu>=2.4.8
|
||||
|
||||
# middleware
|
||||
WebOb>=1.2.3
|
||||
|
190
tests/notify/test_middleware.py
Normal file
190
tests/notify/test_middleware.py
Normal file
@ -0,0 +1,190 @@
|
||||
# Copyright 2013-2014 eNovance
|
||||
# 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 uuid
|
||||
|
||||
import mock
|
||||
import webob
|
||||
|
||||
from oslo.messaging.notify import middleware
|
||||
from tests import utils
|
||||
|
||||
|
||||
class FakeApp(object):
|
||||
def __call__(self, env, start_response):
|
||||
body = 'Some response'
|
||||
start_response('200 OK', [
|
||||
('Content-Type', 'text/plain'),
|
||||
('Content-Length', str(sum(map(len, body))))
|
||||
])
|
||||
return [body]
|
||||
|
||||
|
||||
class FakeFailingApp(object):
|
||||
def __call__(self, env, start_response):
|
||||
raise Exception("It happens!")
|
||||
|
||||
|
||||
class NotifierMiddlewareTest(utils.BaseTestCase):
|
||||
|
||||
def test_notification(self):
|
||||
m = middleware.RequestNotifier(FakeApp())
|
||||
req = webob.Request.blank('/foo/bar',
|
||||
environ={'REQUEST_METHOD': 'GET',
|
||||
'HTTP_X_AUTH_TOKEN': uuid.uuid4()})
|
||||
with mock.patch(
|
||||
'oslo.messaging.notify.notifier.Notifier._notify') as notify:
|
||||
m(req)
|
||||
# Check first notification with only 'request'
|
||||
call_args = notify.call_args_list[0][0]
|
||||
self.assertEqual(call_args[1], 'http.request')
|
||||
self.assertEqual(call_args[3], 'INFO')
|
||||
self.assertEqual(set(call_args[2].keys()),
|
||||
set(['request']))
|
||||
|
||||
request = call_args[2]['request']
|
||||
self.assertEqual(request['PATH_INFO'], '/foo/bar')
|
||||
self.assertEqual(request['REQUEST_METHOD'], 'GET')
|
||||
self.assertIn('HTTP_X_SERVICE_NAME', request)
|
||||
self.assertNotIn('HTTP_X_AUTH_TOKEN', request)
|
||||
self.assertFalse(any(map(lambda s: s.startswith('wsgi.'),
|
||||
request.keys())),
|
||||
"WSGI fields are filtered out")
|
||||
|
||||
# Check second notification with request + response
|
||||
call_args = notify.call_args_list[1][0]
|
||||
self.assertEqual(call_args[1], 'http.response')
|
||||
self.assertEqual(call_args[3], 'INFO')
|
||||
self.assertEqual(set(call_args[2].keys()),
|
||||
set(['request', 'response']))
|
||||
|
||||
request = call_args[2]['request']
|
||||
self.assertEqual(request['PATH_INFO'], '/foo/bar')
|
||||
self.assertEqual(request['REQUEST_METHOD'], 'GET')
|
||||
self.assertIn('HTTP_X_SERVICE_NAME', request)
|
||||
self.assertNotIn('HTTP_X_AUTH_TOKEN', request)
|
||||
self.assertFalse(any(map(lambda s: s.startswith('wsgi.'),
|
||||
request.keys())),
|
||||
"WSGI fields are filtered out")
|
||||
|
||||
response = call_args[2]['response']
|
||||
self.assertEqual(response['status'], '200 OK')
|
||||
self.assertEqual(response['headers']['content-length'], '13')
|
||||
|
||||
def test_notification_response_failure(self):
|
||||
m = middleware.RequestNotifier(FakeFailingApp())
|
||||
req = webob.Request.blank('/foo/bar',
|
||||
environ={'REQUEST_METHOD': 'GET',
|
||||
'HTTP_X_AUTH_TOKEN': uuid.uuid4()})
|
||||
with mock.patch(
|
||||
'oslo.messaging.notify.notifier.Notifier._notify') as notify:
|
||||
try:
|
||||
m(req)
|
||||
self.fail("Application exception has not been re-raised")
|
||||
except Exception:
|
||||
pass
|
||||
# Check first notification with only 'request'
|
||||
call_args = notify.call_args_list[0][0]
|
||||
self.assertEqual(call_args[1], 'http.request')
|
||||
self.assertEqual(call_args[3], 'INFO')
|
||||
self.assertEqual(set(call_args[2].keys()),
|
||||
set(['request']))
|
||||
|
||||
request = call_args[2]['request']
|
||||
self.assertEqual(request['PATH_INFO'], '/foo/bar')
|
||||
self.assertEqual(request['REQUEST_METHOD'], 'GET')
|
||||
self.assertIn('HTTP_X_SERVICE_NAME', request)
|
||||
self.assertNotIn('HTTP_X_AUTH_TOKEN', request)
|
||||
self.assertFalse(any(map(lambda s: s.startswith('wsgi.'),
|
||||
request.keys())),
|
||||
"WSGI fields are filtered out")
|
||||
|
||||
# Check second notification with 'request' and 'exception'
|
||||
call_args = notify.call_args_list[1][0]
|
||||
self.assertEqual(call_args[1], 'http.response')
|
||||
self.assertEqual(call_args[3], 'INFO')
|
||||
self.assertEqual(set(call_args[2].keys()),
|
||||
set(['request', 'exception']))
|
||||
|
||||
request = call_args[2]['request']
|
||||
self.assertEqual(request['PATH_INFO'], '/foo/bar')
|
||||
self.assertEqual(request['REQUEST_METHOD'], 'GET')
|
||||
self.assertIn('HTTP_X_SERVICE_NAME', request)
|
||||
self.assertNotIn('HTTP_X_AUTH_TOKEN', request)
|
||||
self.assertFalse(any(map(lambda s: s.startswith('wsgi.'),
|
||||
request.keys())),
|
||||
"WSGI fields are filtered out")
|
||||
|
||||
exception = call_args[2]['exception']
|
||||
self.assertIn('middleware.py', exception['traceback'][0])
|
||||
self.assertIn('It happens!', exception['traceback'][-1])
|
||||
self.assertEqual(exception['value'], "Exception('It happens!',)")
|
||||
|
||||
def test_process_request_fail(self):
|
||||
def notify_error(context, publisher_id, event_type,
|
||||
priority, payload):
|
||||
raise Exception('error')
|
||||
with mock.patch('oslo.messaging.notify.notifier.Notifier._notify',
|
||||
notify_error):
|
||||
m = middleware.RequestNotifier(FakeApp())
|
||||
req = webob.Request.blank('/foo/bar',
|
||||
environ={'REQUEST_METHOD': 'GET'})
|
||||
m.process_request(req)
|
||||
|
||||
def test_process_response_fail(self):
|
||||
def notify_error(context, publisher_id, event_type,
|
||||
priority, payload):
|
||||
raise Exception('error')
|
||||
with mock.patch('oslo.messaging.notify.notifier.Notifier._notify',
|
||||
notify_error):
|
||||
m = middleware.RequestNotifier(FakeApp())
|
||||
req = webob.Request.blank('/foo/bar',
|
||||
environ={'REQUEST_METHOD': 'GET'})
|
||||
m.process_response(req, webob.response.Response())
|
||||
|
||||
def test_ignore_req_opt(self):
|
||||
m = middleware.RequestNotifier(FakeApp(),
|
||||
ignore_req_list='get, PUT')
|
||||
req = webob.Request.blank('/skip/foo',
|
||||
environ={'REQUEST_METHOD': 'GET'})
|
||||
req1 = webob.Request.blank('/skip/foo',
|
||||
environ={'REQUEST_METHOD': 'PUT'})
|
||||
req2 = webob.Request.blank('/accept/foo',
|
||||
environ={'REQUEST_METHOD': 'POST'})
|
||||
with mock.patch(
|
||||
'oslo.messaging.notify.notifier.Notifier._notify') as notify:
|
||||
# Check GET request does not send notification
|
||||
m(req)
|
||||
m(req1)
|
||||
self.assertEqual(len(notify.call_args_list), 0)
|
||||
|
||||
# Check non-GET request does send notification
|
||||
m(req2)
|
||||
self.assertEqual(len(notify.call_args_list), 2)
|
||||
call_args = notify.call_args_list[0][0]
|
||||
self.assertEqual(call_args[1], 'http.request')
|
||||
self.assertEqual(call_args[3], 'INFO')
|
||||
self.assertEqual(set(call_args[2].keys()),
|
||||
set(['request']))
|
||||
|
||||
request = call_args[2]['request']
|
||||
self.assertEqual(request['PATH_INFO'], '/accept/foo')
|
||||
self.assertEqual(request['REQUEST_METHOD'], 'POST')
|
||||
|
||||
call_args = notify.call_args_list[1][0]
|
||||
self.assertEqual(call_args[1], 'http.response')
|
||||
self.assertEqual(call_args[3], 'INFO')
|
||||
self.assertEqual(set(call_args[2].keys()),
|
||||
set(['request', 'response']))
|
Loading…
Reference in New Issue
Block a user