|
1 | | -from unittest.mock import patch |
| 1 | +import io |
| 2 | +import logging |
| 3 | +from unittest.mock import patch, Mock |
2 | 4 |
|
3 | 5 | import django |
4 | 6 | from django.contrib.auth import get_user_model |
|
17 | 19 | from rest_framework.views import APIView |
18 | 20 |
|
19 | 21 | from requestlogs import get_requestlog_entry |
| 22 | +from requestlogs.logging import RequestIdContext |
20 | 23 | from requestlogs.storages import BaseEntrySerializer |
21 | 24 |
|
22 | 25 |
|
@@ -52,6 +55,11 @@ def retrieve(self, request): |
52 | 55 | def create(self, request): |
53 | 56 | return Response({}) |
54 | 57 |
|
| 58 | + def w_logging(self, request): |
| 59 | + logger = logging.getLogger('request_id_test') |
| 60 | + logger.info('GET with logging ({})'.format(request.GET.get('q'))) |
| 61 | + return Response({}) |
| 62 | + |
55 | 63 |
|
56 | 64 | class SimpleAuth(BaseAuthentication): |
57 | 65 | def authenticate(self, request): |
@@ -100,6 +108,7 @@ def api_view_function(request): |
100 | 108 | url(r'^viewset/1/?$', ViewSet.as_view({'get': 'retrieve'})), |
101 | 109 | url(r'^func/?$', api_view_function), |
102 | 110 | url(r'^error/?$', ServerErrorView.as_view()), |
| 111 | + url(r'^logging/?$', ViewSet.as_view({'get': 'w_logging'})), |
103 | 112 | ] |
104 | 113 |
|
105 | 114 |
|
@@ -389,3 +398,127 @@ def test_500_response_without_custom_exception_handler(self): |
389 | 398 | KeyError, self.client.post, '/error', {'pay': 'load'}) |
390 | 399 | self.expected['request']['data'] = None |
391 | 400 | self.assert_stored(mocked_store, self.expected) |
| 401 | + |
| 402 | + |
| 403 | +class RequestIdStorage(TestStorage): |
| 404 | + class serializer_class(serializers.Serializer): |
| 405 | + request_id = serializers.CharField(source='request.request_id') |
| 406 | + |
| 407 | + |
| 408 | +class LoggingMixin(object): |
| 409 | + def _setup_logging(self): |
| 410 | + log_format = '{levelname} {request_id} {message}' |
| 411 | + stream = io.StringIO('') |
| 412 | + handler = logging.StreamHandler(stream) |
| 413 | + handler.setFormatter(logging.Formatter(log_format, style='{')) |
| 414 | + logger = logging.getLogger('request_id_test') |
| 415 | + logger.setLevel(logging.INFO) |
| 416 | + logger.addHandler(handler) |
| 417 | + logger.addFilter(RequestIdContext()) |
| 418 | + self._log_stream = stream |
| 419 | + |
| 420 | + def _assert_logged_lines(self, lines): |
| 421 | + self._log_stream.seek(0) |
| 422 | + raw = self._log_stream.read() |
| 423 | + assert raw.endswith('\n') |
| 424 | + logged_lines = [i for i in raw.split('\n')][:-1] |
| 425 | + assert logged_lines == lines |
| 426 | + |
| 427 | + |
| 428 | +@override_settings( |
| 429 | + ROOT_URLCONF=__name__, |
| 430 | + REQUESTLOGS={'STORAGE_CLASS': 'tests.test_views.RequestIdStorage'}, |
| 431 | +) |
| 432 | +@modify_settings(MIDDLEWARE={ |
| 433 | + 'append': [ |
| 434 | + 'requestlogs.middleware.RequestLogsMiddleware', |
| 435 | + 'requestlogs.middleware.RequestIdMiddleware', |
| 436 | + ], |
| 437 | +}) |
| 438 | +class TestRequestIdMiddleware(LoggingMixin, APITestCase): |
| 439 | + def test_request_id_generated(self): |
| 440 | + with patch('tests.test_views.RequestIdStorage.do_store') \ |
| 441 | + as mocked_store, patch('uuid.uuid4') as mocked_uuid: |
| 442 | + mocked_uuid.side_effect = [Mock(hex='12345dcba')] |
| 443 | + response = self.client.get('/') |
| 444 | + assert mocked_store.call_args[0][0] == {'request_id': '12345dcba'} |
| 445 | + |
| 446 | + def test_python_logging_with_request_id(self): |
| 447 | + # First build logging setup which outputs entries with request_id. |
| 448 | + # We cannot use `self.assertLogs`, because it uses the default |
| 449 | + # formatter, and so request_id wouldn't be seen in log output. |
| 450 | + self._setup_logging() |
| 451 | + |
| 452 | + # Now that logging is set up to capture formatted log entries into the |
| 453 | + # `stream`, we can do the request and finally check the logged result. |
| 454 | + with patch('uuid.uuid4') as mocked_uuid: |
| 455 | + mocked_uuid.side_effect = [Mock(hex='12345dcba')] |
| 456 | + self.client.get('/logging?q=yes') |
| 457 | + |
| 458 | + self._assert_logged_lines(['INFO 12345dcba GET with logging (yes)']) |
| 459 | + |
| 460 | + def test_python_logging_and_requestlogs_entry(self): |
| 461 | + self._setup_logging() |
| 462 | + |
| 463 | + with patch('tests.test_views.RequestIdStorage.do_store') \ |
| 464 | + as mocked_store, patch('uuid.uuid4') as mocked_uuid, \ |
| 465 | + patch('uuid.uuid4') as mocked_uuid: |
| 466 | + mocked_uuid.side_effect = [ |
| 467 | + Mock(hex='12345dcba'), Mock(hex='ffc999123')] |
| 468 | + self.client.get('/logging?q=1') |
| 469 | + self.client.get('/logging?q=2') |
| 470 | + |
| 471 | + call_args1, call_args2 = mocked_store.call_args_list |
| 472 | + assert call_args1[0][0] == {'request_id': '12345dcba'} |
| 473 | + assert call_args2[0][0] == {'request_id': 'ffc999123'} |
| 474 | + |
| 475 | + self._assert_logged_lines([ |
| 476 | + 'INFO 12345dcba GET with logging (1)', |
| 477 | + 'INFO ffc999123 GET with logging (2)', |
| 478 | + ]) |
| 479 | + |
| 480 | + |
| 481 | +@override_settings( |
| 482 | + ROOT_URLCONF=__name__, |
| 483 | + REQUESTLOGS={ |
| 484 | + 'STORAGE_CLASS': 'tests.test_views.RequestIdStorage', |
| 485 | + 'REQUEST_ID_HTTP_HEADER': 'X_DJANGO_REQUEST_ID', |
| 486 | + }, |
| 487 | +) |
| 488 | +@modify_settings(MIDDLEWARE={ |
| 489 | + 'append': [ |
| 490 | + 'requestlogs.middleware.RequestLogsMiddleware', |
| 491 | + 'requestlogs.middleware.RequestIdMiddleware', |
| 492 | + ], |
| 493 | +}) |
| 494 | +class TestReuseRequestId(LoggingMixin, APITestCase): |
| 495 | + def test_reuse_request_id(self): |
| 496 | + self._setup_logging() |
| 497 | + |
| 498 | + uuid = '6359abe9f7d849e09a324791c6a6c976' |
| 499 | + self.client.credentials(X_DJANGO_REQUEST_ID=uuid) |
| 500 | + |
| 501 | + with patch('tests.test_views.RequestIdStorage.do_store') \ |
| 502 | + as mocked_store, patch('uuid.uuid4') as mocked_uuid, \ |
| 503 | + patch('uuid.uuid4') as mocked_uuid: |
| 504 | + mocked_uuid.side_effect = [] |
| 505 | + response = self.client.get('/logging?q=p') |
| 506 | + assert mocked_store.call_args[0][0] == {'request_id': uuid} |
| 507 | + |
| 508 | + self._assert_logged_lines([ |
| 509 | + 'INFO 6359abe9f7d849e09a324791c6a6c976 GET with logging (p)']) |
| 510 | + |
| 511 | + def test_bad_request_id(self): |
| 512 | + self.client.credentials(X_DJANGO_REQUEST_ID='BAD') |
| 513 | + self._test_request_id_generated() |
| 514 | + |
| 515 | + def test_no_request_id_present(self): |
| 516 | + self._test_request_id_generated() |
| 517 | + |
| 518 | + def _test_request_id_generated(self): |
| 519 | + with patch('tests.test_views.RequestIdStorage.do_store') \ |
| 520 | + as mocked_store, patch('uuid.uuid4') as mocked_uuid, \ |
| 521 | + patch('uuid.uuid4') as mocked_uuid: |
| 522 | + mocked_uuid.side_effect = [Mock(hex='12345dcba')] |
| 523 | + response = self.client.get('/') |
| 524 | + assert mocked_store.call_args[0][0] == {'request_id': '12345dcba'} |
0 commit comments