request_log addition for running under uwsgi
If someone chooses to run under uwsgi/apache instead of eventlet, the basic logging of requests goes away (as that was an eventlet.wsgi function). This is a critical piece of information for understanding how services are working, and we need to retain it under uwsgi/apache. This creates a new request_log middleware, inspired by the one in placement, to provide that functionality. This includes all the same information as before: http method, uri, status, content length, time for the request. It also includes the microversion the request was processed as, "-" if no microversion. The middleware does not emit anything if it detects that it's running under eventlet, to prevent duplicate log messages. Release notes provided as this will be a manual transition for folks as it's an api-paste.ini change. Change-Id: I3a597b06d3501c765e2d7805c6c1375d6f4e40db
This commit is contained in:
parent
2e4417d57c
commit
aa45a6f3ab
@ -28,17 +28,20 @@ use = call:nova.api.openstack.urlmap:urlmap_factory
|
||||
|
||||
[composite:openstack_compute_api_v21]
|
||||
use = call:nova.api.auth:pipeline_factory_v21
|
||||
noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler noauth2 osapi_compute_app_v21
|
||||
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler authtoken keystonecontext osapi_compute_app_v21
|
||||
noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap request_log sizelimit osprofiler noauth2 osapi_compute_app_v21
|
||||
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap request_log sizelimit osprofiler authtoken keystonecontext osapi_compute_app_v21
|
||||
|
||||
[composite:openstack_compute_api_v21_legacy_v2_compatible]
|
||||
use = call:nova.api.auth:pipeline_factory_v21
|
||||
noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler noauth2 legacy_v2_compatible osapi_compute_app_v21
|
||||
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler authtoken keystonecontext legacy_v2_compatible osapi_compute_app_v21
|
||||
noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap request_log sizelimit osprofiler noauth2 legacy_v2_compatible osapi_compute_app_v21
|
||||
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap request_log sizelimit osprofiler authtoken keystonecontext legacy_v2_compatible osapi_compute_app_v21
|
||||
|
||||
[filter:request_id]
|
||||
paste.filter_factory = oslo_middleware:RequestId.factory
|
||||
|
||||
[filter:request_log]
|
||||
paste.filter_factory = nova.api.openstack.requestlog:RequestLog.factory
|
||||
|
||||
[filter:compute_req_id]
|
||||
paste.filter_factory = nova.api.compute_req_id:ComputeReqIdMiddleware.factory
|
||||
|
||||
@ -64,7 +67,7 @@ paste.filter_factory = nova.api.openstack:LegacyV2CompatibleWrapper.factory
|
||||
paste.app_factory = nova.api.openstack.compute:APIRouterV21.factory
|
||||
|
||||
[pipeline:oscomputeversions]
|
||||
pipeline = cors faultwrap http_proxy_to_wsgi oscomputeversionapp
|
||||
pipeline = cors faultwrap request_log http_proxy_to_wsgi oscomputeversionapp
|
||||
|
||||
[app:oscomputeversionapp]
|
||||
paste.app_factory = nova.api.openstack.compute.versions:Versions.factory
|
||||
|
92
nova/api/openstack/requestlog.py
Normal file
92
nova/api/openstack/requestlog.py
Normal file
@ -0,0 +1,92 @@
|
||||
# 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.
|
||||
"""Simple middleware for request logging."""
|
||||
|
||||
import time
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from nova.api.openstack import wsgi
|
||||
from nova import wsgi as base_wsgi
|
||||
|
||||
# TODO(sdague) maybe we can use a better name here for the logger
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestLog(base_wsgi.Middleware):
|
||||
"""WSGI Middleware to write a simple request log to.
|
||||
|
||||
Borrowed from Paste Translogger
|
||||
"""
|
||||
|
||||
# This matches the placement fil
|
||||
_log_format = ('%(REMOTE_ADDR)s "%(REQUEST_METHOD)s %(REQUEST_URI)s" '
|
||||
'status: %(status)s len: %(len)s '
|
||||
'microversion: %(microversion)s time: %(time).6f')
|
||||
|
||||
@staticmethod
|
||||
def _get_uri(environ):
|
||||
req_uri = (environ.get('SCRIPT_NAME', '') +
|
||||
environ.get('PATH_INFO', ''))
|
||||
if environ.get('QUERY_STRING'):
|
||||
req_uri += '?' + environ['QUERY_STRING']
|
||||
return req_uri
|
||||
|
||||
@staticmethod
|
||||
def _should_emit(req):
|
||||
"""Conditions under which we should skip logging.
|
||||
|
||||
If we detect we are running under eventlet wsgi processing, we
|
||||
already are logging things, let it go. This is broken out as a
|
||||
separate method so that it can be easily adjusted for testing.
|
||||
"""
|
||||
if req.environ.get('eventlet.input', None) is not None:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _log_req(self, req, res, start):
|
||||
if not self._should_emit(req):
|
||||
return
|
||||
|
||||
# in the event that things exploded really badly deeper in the
|
||||
# wsgi stack, res is going to be an empty dict for the
|
||||
# fallback logging. So never count on it having attributes.
|
||||
status = getattr(res, "status", "500 Error").split(None, 1)[0]
|
||||
data = {
|
||||
'REMOTE_ADDR': req.environ.get('REMOTE_ADDR', '-'),
|
||||
'REQUEST_METHOD': req.environ['REQUEST_METHOD'],
|
||||
'REQUEST_URI': self._get_uri(req.environ),
|
||||
'status': status,
|
||||
'len': getattr(res, "content_length", 0),
|
||||
'time': time.time() - start,
|
||||
'microversion': '-'
|
||||
}
|
||||
# set microversion if it exists
|
||||
if not req.api_version_request.is_null():
|
||||
data["microversion"] = req.api_version_request.get_string()
|
||||
LOG.info(self._log_format, data)
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
res = {}
|
||||
start = time.time()
|
||||
try:
|
||||
res = req.get_response(self.application)
|
||||
self._log_req(req, res, start)
|
||||
return res
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
self._log_req(req, res, start)
|
157
nova/tests/unit/api/openstack/test_requestlog.py
Normal file
157
nova/tests/unit/api/openstack/test_requestlog.py
Normal file
@ -0,0 +1,157 @@
|
||||
# Copyright 2017 IBM Corp.
|
||||
# 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 mock
|
||||
|
||||
import fixtures as fx
|
||||
import testtools
|
||||
|
||||
from nova.tests import fixtures
|
||||
from nova.tests.unit import conf_fixture
|
||||
|
||||
"""Test request logging middleware under various conditions.
|
||||
|
||||
The request logging middleware is needed when running under something
|
||||
other than eventlet. While Nova grew up on eventlet, and it's wsgi
|
||||
server, it meant that our user facing data (the log stream) was a mix
|
||||
of what Nova was emitting, and what eventlet.wsgi was emitting on our
|
||||
behalf. When running under uwsgi we want to make sure that we have
|
||||
equivalent coverage.
|
||||
|
||||
All these tests use GET / to hit an endpoint that doesn't require the
|
||||
database setup. We have to do a bit of mocking to make that work.
|
||||
"""
|
||||
|
||||
|
||||
class TestRequestLogMiddleware(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestRequestLogMiddleware, self).setUp()
|
||||
# this is the minimal set of magic mocks needed to convince
|
||||
# the API service it can start on it's own without a database.
|
||||
mocks = ['nova.objects.Service.get_by_host_and_binary',
|
||||
'nova.objects.Service.create']
|
||||
for m in mocks:
|
||||
p = mock.patch(m)
|
||||
self.addCleanup(p.stop)
|
||||
p.start()
|
||||
|
||||
@mock.patch('nova.api.openstack.requestlog.RequestLog._should_emit')
|
||||
def test_logs_requests(self, emit):
|
||||
"""Ensure requests are logged.
|
||||
|
||||
Make a standard request for / and ensure there is a log entry.
|
||||
"""
|
||||
|
||||
emit.return_value = True
|
||||
self.useFixture(fixtures.OutputStreamCapture())
|
||||
log = fixtures.StandardLogging()
|
||||
self.useFixture(log)
|
||||
self.useFixture(conf_fixture.ConfFixture())
|
||||
self.useFixture(fixtures.RPCFixture('nova.test'))
|
||||
api = self.useFixture(fixtures.OSAPIFixture()).api
|
||||
|
||||
resp = api.api_request('/', strip_version=True)
|
||||
log1 = ('INFO [nova.api.openstack.requestlog] 127.0.0.1 '
|
||||
'"GET /v2" status: 204 len: 0 microversion: - time:')
|
||||
self.assertIn(log1, log.logger.output)
|
||||
|
||||
# the content length might vary, but the important part is
|
||||
# what we log is what we return to the user (which turns out
|
||||
# to excitingly not be the case with eventlet!)
|
||||
content_length = resp.headers['content-length']
|
||||
|
||||
log2 = ('INFO [nova.api.openstack.requestlog] 127.0.0.1 '
|
||||
'"GET /" status: 200 len: %s' % content_length)
|
||||
self.assertIn(log2, log.logger.output)
|
||||
|
||||
@mock.patch('nova.api.openstack.requestlog.RequestLog._should_emit')
|
||||
def test_logs_mv(self, emit):
|
||||
"""Ensure logs register microversion if passed.
|
||||
|
||||
This makes sure that microversion logging actually shows up
|
||||
when appropriate.
|
||||
"""
|
||||
|
||||
emit.return_value = True
|
||||
self.useFixture(fixtures.OutputStreamCapture())
|
||||
log = fixtures.StandardLogging()
|
||||
self.useFixture(log)
|
||||
self.useFixture(conf_fixture.ConfFixture())
|
||||
# NOTE(sdague): all these tests are using the
|
||||
self.useFixture(
|
||||
fx.MonkeyPatch(
|
||||
'nova.api.openstack.compute.versions.'
|
||||
'Versions.support_api_request_version',
|
||||
True))
|
||||
|
||||
self.useFixture(fixtures.RPCFixture('nova.test'))
|
||||
|
||||
api = self.useFixture(fixtures.OSAPIFixture()).api
|
||||
api.microversion = '2.25'
|
||||
|
||||
resp = api.api_request('/', strip_version=True)
|
||||
content_length = resp.headers['content-length']
|
||||
|
||||
log1 = ('INFO [nova.api.openstack.requestlog] 127.0.0.1 '
|
||||
'"GET /" status: 200 len: %s microversion: 2.25 time:' %
|
||||
content_length)
|
||||
self.assertIn(log1, log.logger.output)
|
||||
|
||||
@mock.patch('nova.api.openstack.compute.versions.Versions.index')
|
||||
@mock.patch('nova.api.openstack.requestlog.RequestLog._should_emit')
|
||||
def test_logs_under_exception(self, emit, v_index):
|
||||
"""Ensure that logs still emit under unexpected failure.
|
||||
|
||||
If we get an unexpected failure all the way up to the top, we should
|
||||
still have a record of that request via the except block.
|
||||
"""
|
||||
|
||||
emit.return_value = True
|
||||
v_index.side_effect = Exception("Unexpected Error")
|
||||
self.useFixture(fixtures.OutputStreamCapture())
|
||||
log = fixtures.StandardLogging()
|
||||
self.useFixture(log)
|
||||
self.useFixture(conf_fixture.ConfFixture())
|
||||
self.useFixture(fixtures.RPCFixture('nova.test'))
|
||||
api = self.useFixture(fixtures.OSAPIFixture()).api
|
||||
|
||||
api.api_request('/', strip_version=True)
|
||||
log1 = ('INFO [nova.api.openstack.requestlog] 127.0.0.1 "GET /"'
|
||||
' status: 500 len: 0 microversion: - time:')
|
||||
self.assertIn(log1, log.logger.output)
|
||||
|
||||
@mock.patch('nova.api.openstack.requestlog.RequestLog._should_emit')
|
||||
def test_no_log_under_eventlet(self, emit):
|
||||
"""Ensure that logs don't end up under eventlet.
|
||||
|
||||
We still set the _should_emit return value directly to prevent
|
||||
the situation where eventlet is removed from tests and this
|
||||
preventing that.
|
||||
|
||||
NOTE(sdague): this test can be deleted when eventlet is no
|
||||
longer supported for the wsgi stack in Nova.
|
||||
"""
|
||||
|
||||
emit.return_value = False
|
||||
self.useFixture(fixtures.OutputStreamCapture())
|
||||
log = fixtures.StandardLogging()
|
||||
self.useFixture(log)
|
||||
self.useFixture(conf_fixture.ConfFixture())
|
||||
self.useFixture(fixtures.RPCFixture('nova.test'))
|
||||
api = self.useFixture(fixtures.OSAPIFixture()).api
|
||||
|
||||
api.api_request('/', strip_version=True)
|
||||
self.assertNotIn("nova.api.openstack.requestlog", log.logger.output)
|
11
releasenotes/notes/request_log-e7680b3276910743.yaml
Normal file
11
releasenotes/notes/request_log-e7680b3276910743.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
upgrade:
|
||||
- |
|
||||
A new request_log middleware is created to log REST HTTP requests
|
||||
even if Nova API is not running under eventlet.wsgi. Because this
|
||||
is an api-paste.ini change, you will need to manually update your
|
||||
api-paste.ini with the one from the release to get this
|
||||
functionality. The new request logs will only emit when it is
|
||||
detected that nova-api is not running under eventlet, and will
|
||||
include the microversion of the request in addition to all the
|
||||
previously logged information.
|
Loading…
Reference in New Issue
Block a user