From 31b994560332ffa22fbe45e43471db360467434c Mon Sep 17 00:00:00 2001
From: Peter Portante <peter.portante@redhat.com>
Date: Tue, 25 Mar 2014 15:56:44 -0400
Subject: [PATCH] Invert which proxy logging middleware instance logs

Prior to this patch the proxy_logging middleware always prepared to
read the response and process it for access logging, where the
proxy_logging instance first to handle the response bytes marked the
request as "logged".

This meant that the proxy_logging instance immediately before the
proxy-server app (right most in the pipeline, if properly setup) would
always log the responses for all the client requests, regardless if
middleware to the left of it still had processing to do for the
response. This would break SLO, where the slo middleware passed along
the GET of the manifest, unchanged from the client, so that the right
most proxy logging middleware would log the sponse for just the
manifest, but then the slo middleware would fetch all the segments of
the manifest, feeding them out in the response to the client, the
request now marked as "logged" so that the left most proxy logging
middleware would not log it.

This patch inverts this behavior so that now the first proxy_logging
middleware instance to receive a request in the pipeline marks that
request as handling it. So now, the left most proxy_logging middleware
handles logging for all client reaquests, and the right most
proxy_logging middleware handles all other requests initiated from
within the pipeline to its left.

Closes bug: 1297438

Change-Id: Ia3561523db76c693e4e0b2c38f461544dfcee086
---
 swift/common/middleware/proxy_logging.py      | 18 ++--
 .../common/middleware/test_proxy_logging.py   | 83 ++++++++++++++++++-
 2 files changed, 92 insertions(+), 9 deletions(-)

diff --git a/swift/common/middleware/proxy_logging.py b/swift/common/middleware/proxy_logging.py
index e3b3b89ae9..0732700622 100644
--- a/swift/common/middleware/proxy_logging.py
+++ b/swift/common/middleware/proxy_logging.py
@@ -124,11 +124,11 @@ class ProxyLoggingMiddleware(object):
     def method_from_req(self, req):
         return req.environ.get('swift.orig_req_method', req.method)
 
-    def req_already_logged(self, req):
-        return req.environ.get('swift.proxy_access_log_made')
+    def req_already_logged(self, env):
+        return env.get('swift.proxy_access_log_made')
 
-    def mark_req_logged(self, req):
-        req.environ['swift.proxy_access_log_made'] = True
+    def mark_req_logged(self, env):
+        env['swift.proxy_access_log_made'] = True
 
     def obscure_sensitive(self, value):
         if value and len(value) > self.reveal_sensitive_prefix:
@@ -147,8 +147,6 @@ class ProxyLoggingMiddleware(object):
         :param start_time: timestamp request started
         :param end_time: timestamp request completed
         """
-        if self.req_already_logged(req):
-            return
         req_path = get_valid_utf8_str(req.path)
         the_request = quote(unquote(req_path), QUOTE_SAFE)
         if req.query_string:
@@ -193,7 +191,6 @@ class ProxyLoggingMiddleware(object):
                 start_time_str,
                 end_time_str
             )))
-        self.mark_req_logged(req)
         # Log timing and bytes-transfered data to StatsD
         metric_name = self.statsd_metric_name(req, status_int, method)
         # Only log data for valid controllers (or SOS) to keep the metric count
@@ -220,6 +217,11 @@ class ProxyLoggingMiddleware(object):
         return '.'.join((stat_type, stat_method, str(status_int)))
 
     def __call__(self, env, start_response):
+        if self.req_already_logged(env):
+            return self.app(env, start_response)
+
+        self.mark_req_logged(env)
+
         start_response_args = [None]
         input_proxy = InputProxy(env['wsgi.input'])
         env['wsgi.input'] = input_proxy
@@ -261,7 +263,7 @@ class ProxyLoggingMiddleware(object):
 
             # Log timing information for time-to-first-byte (GET requests only)
             method = self.method_from_req(req)
-            if method == 'GET' and not self.req_already_logged(req):
+            if method == 'GET':
                 status_int = status_int_for_logging()
                 metric_name = self.statsd_metric_name(req, status_int, method)
                 if metric_name:
diff --git a/test/unit/common/middleware/test_proxy_logging.py b/test/unit/common/middleware/test_proxy_logging.py
index 7e0744a4be..c577fd07e0 100644
--- a/test/unit/common/middleware/test_proxy_logging.py
+++ b/test/unit/common/middleware/test_proxy_logging.py
@@ -22,7 +22,7 @@ import mock
 from test.unit import FakeLogger
 from swift.common.utils import get_logger
 from swift.common.middleware import proxy_logging
-from swift.common.swob import Request
+from swift.common.swob import Request, Response
 
 
 class FakeApp(object):
@@ -757,6 +757,87 @@ class TestProxyLogging(unittest.TestCase):
         self.assertEquals(log_parts[18], '10000000.000000000')
         self.assertEquals(log_parts[19], '10000001.000000000')
 
+    def test_dual_logging_middlewares(self):
+        # Since no internal request is being made, outer most proxy logging
+        # middleware, log1, should have performed the logging.
+        app = FakeApp()
+        flg0 = FakeLogger()
+        env = {}
+        log0 = proxy_logging.ProxyLoggingMiddleware(app, env, logger=flg0)
+        flg1 = FakeLogger()
+        log1 = proxy_logging.ProxyLoggingMiddleware(log0, env, logger=flg1)
+
+        req = Request.blank('/', environ={'REQUEST_METHOD': 'GET'})
+        resp = log1(req.environ, start_response)
+        resp_body = ''.join(resp)
+        self._log_parts(log0, should_be_empty=True)
+        log_parts = self._log_parts(log1)
+        self.assertEquals(log_parts[3], 'GET')
+        self.assertEquals(log_parts[4], '/')
+        self.assertEquals(log_parts[5], 'HTTP/1.0')
+        self.assertEquals(log_parts[6], '200')
+        self.assertEquals(resp_body, 'FAKE APP')
+        self.assertEquals(log_parts[11], str(len(resp_body)))
+
+    def test_dual_logging_middlewares_w_inner(self):
+
+        class FakeMiddleware(object):
+            """
+            Fake middleware to make a separate internal request, but construct
+            the response with different data.
+            """
+            def __init__(self, app, conf):
+                self.app = app
+                self.conf = conf
+
+            def GET(self, req):
+                # Make the internal request
+                ireq = Request.blank('/', environ={'REQUEST_METHOD': 'GET'})
+                resp = self.app(ireq.environ, start_response)
+                resp_body = ''.join(resp)
+                if resp_body != 'FAKE APP':
+                    return Response(request=req,
+                                    body="FAKE APP WAS NOT RETURNED",
+                                    content_type="text/plain")
+                # But our response is different
+                return Response(request=req, body="FAKE MIDDLEWARE",
+                                content_type="text/plain")
+
+            def __call__(self, env, start_response):
+                req = Request(env)
+                return self.GET(req)(env, start_response)
+
+        # Since an internal request is being made, inner most proxy logging
+        # middleware, log0, should have performed the logging.
+        app = FakeApp()
+        flg0 = FakeLogger()
+        env = {}
+        log0 = proxy_logging.ProxyLoggingMiddleware(app, env, logger=flg0)
+        fake = FakeMiddleware(log0, env)
+        flg1 = FakeLogger()
+        log1 = proxy_logging.ProxyLoggingMiddleware(fake, env, logger=flg1)
+
+        req = Request.blank('/', environ={'REQUEST_METHOD': 'GET'})
+        resp = log1(req.environ, start_response)
+        resp_body = ''.join(resp)
+
+        # Inner most logger should have logged the app's response
+        log_parts = self._log_parts(log0)
+        self.assertEquals(log_parts[3], 'GET')
+        self.assertEquals(log_parts[4], '/')
+        self.assertEquals(log_parts[5], 'HTTP/1.0')
+        self.assertEquals(log_parts[6], '200')
+        self.assertEquals(log_parts[11], str(len('FAKE APP')))
+
+        # Outer most logger should have logged the other middleware's response
+        log_parts = self._log_parts(log1)
+        self.assertEquals(log_parts[3], 'GET')
+        self.assertEquals(log_parts[4], '/')
+        self.assertEquals(log_parts[5], 'HTTP/1.0')
+        self.assertEquals(log_parts[6], '200')
+        self.assertEquals(resp_body, 'FAKE MIDDLEWARE')
+        self.assertEquals(log_parts[11], str(len(resp_body)))
+
 
 if __name__ == '__main__':
     unittest.main()