diff --git a/oslo/messaging/notify/middleware.py b/oslo/messaging/notify/middleware.py
new file mode 100644
index 000000000..1bf0c66be
--- /dev/null
+++ b/oslo/messaging/notify/middleware.py
@@ -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
diff --git a/requirements.txt b/requirements.txt
index 932b534b9..e36cd3975 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -22,3 +22,6 @@ PyYAML>=3.1.0
 
 # rabbit driver is the default
 kombu>=2.4.8
+
+# middleware
+WebOb>=1.2.3
diff --git a/tests/notify/test_middleware.py b/tests/notify/test_middleware.py
new file mode 100644
index 000000000..0eb3cac2e
--- /dev/null
+++ b/tests/notify/test_middleware.py
@@ -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']))