├── .circleci └── config.yml ├── .gitignore ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── examples ├── server_with_celery │ ├── Readme.md │ ├── example_app │ │ ├── __init__.py │ │ ├── celery.py │ │ └── tasks.py │ ├── web.py │ └── worker.py └── server_with_logging.py ├── flask_log_request_id ├── __init__.py ├── ctx_fetcher.py ├── extras │ ├── __init__.py │ └── celery.py ├── filters.py ├── parser.py └── request_id.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── ctx_fetcher_tests.py ├── extras ├── __init__.py └── celery_tests.py ├── parser_tests.py └── request_id_tests.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | test-3.6: &test-template 5 | # Job to run tests test on python 3.6 6 | docker: 7 | - image: circleci/python:3.6 8 | 9 | environment: 10 | PYTHON_BIN: python3 11 | PYTHON_VERSION: 3.6 12 | 13 | steps: 14 | - checkout 15 | 16 | # Download and cache dependencies 17 | - restore_cache: 18 | keys: 19 | - v2-test-dependencies-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }} 20 | 21 | - run: 22 | name: Install test dependencies 23 | command: | 24 | ${PYTHON_BIN} -m venv venv 25 | . venv/bin/activate 26 | pip install -U pip 27 | pip install -U setuptools 28 | # Install package with dependencies 29 | pip install .[test] 30 | 31 | - save_cache: 32 | paths: 33 | - ./venv 34 | key: v2-test-dependencies-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }} 35 | 36 | # run Flake8! 37 | - run: 38 | name: Flake8 Compliance 39 | command: | 40 | . venv/bin/activate 41 | ${PYTHON_BIN} setup.py flake8 42 | 43 | # run tests! 44 | - run: 45 | name: Unit-Tests 46 | command: | 47 | . venv/bin/activate 48 | ${PYTHON_BIN} setup.py nosetests 49 | 50 | test-3.5: 51 | # Clone-job to run tests test on python 3.6 52 | <<: *test-template 53 | docker: 54 | - image: circleci/python:3.5 55 | 56 | environment: 57 | PYTHON_BIN: python3 58 | PYTHON_VERSION: 3.5 59 | 60 | build: 61 | # Build and store artifacts 62 | 63 | docker: 64 | - image: circleci/python:3.6 65 | 66 | steps: 67 | - checkout 68 | - run: 69 | name: Create environment 70 | command: | 71 | python3 -m venv venv 72 | . venv/bin/activate 73 | pip install -U pip setuptools wheel 74 | 75 | - run: 76 | name: Build Source and Wheel distribution files 77 | command: | 78 | . venv/bin/activate 79 | python setup.py sdist bdist_wheel 80 | python setup.py --version > version.txt 81 | 82 | - store_artifacts: 83 | path: dist 84 | destination: dist 85 | 86 | - persist_to_workspace: 87 | root: . 88 | paths: 89 | - dist 90 | - version.txt 91 | 92 | deploy_test_pypi: &deploy_template 93 | # Job to deploy on test environment 94 | # Requires dist files under workspace/dist 95 | docker: 96 | - image: circleci/python:3.6 97 | 98 | steps: 99 | 100 | - attach_workspace: 101 | at: workspace 102 | 103 | - run: 104 | name: Create VEnv with tools 105 | command: | 106 | python3 -m venv venv 107 | . venv/bin/activate 108 | pip install -U pip setuptools twine 109 | 110 | - run: 111 | name: Upload to PyPI 112 | command: | 113 | . venv/bin/activate 114 | export TWINE_USERNAME="${PYPI_TEST_USERNAME}" 115 | export TWINE_PASSWORD="${PYPI_TEST_PASSWORD}" 116 | echo "Uploading to test.pypi.org" 117 | ./venv/bin/twine upload --repository-url https://test.pypi.org/legacy/ workspace/dist/*`cat workspace/version.txt`* 118 | 119 | deploy_pypi: 120 | # Job to deploy on public PyPI 121 | # Requires dist files under workspace/dist 122 | docker: 123 | - image: circleci/python:3.6 124 | 125 | steps: 126 | 127 | - attach_workspace: 128 | at: workspace 129 | 130 | - run: 131 | name: Create VEnv with tools 132 | command: | 133 | python3 -m venv venv 134 | . venv/bin/activate 135 | pip install -U pip setuptools twine 136 | 137 | - run: 138 | name: Upload to PyPI 139 | command: | 140 | . venv/bin/activate 141 | export TWINE_USERNAME="${PYPI_USERNAME}" 142 | export TWINE_PASSWORD="${PYPI_PASSWORD}" 143 | echo "Uploading to pypi.org" 144 | ./venv/bin/twine upload workspace/dist/*`cat workspace/version.txt`* 145 | 146 | 147 | install_from_test_pypi: 148 | # Try to install from test-pypi 149 | docker: 150 | - image: circleci/python:3.6 151 | 152 | steps: 153 | - attach_workspace: 154 | at: workspace 155 | 156 | - run: 157 | name: Create virtual-environment 158 | command: | 159 | python3 -m venv venv 160 | . venv/bin/activate 161 | 162 | - run: 163 | name: Install from pip 164 | # Try to install the same version from PyPI 165 | command: | 166 | . venv/bin/activate 167 | pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple flask-log-request-id==`cat workspace/version.txt` 168 | pip freeze | grep -i flask-log-request-id 169 | 170 | install_from_pypi: 171 | # Try to install from public pypi 172 | docker: 173 | - image: circleci/python:3.6 174 | 175 | steps: 176 | - attach_workspace: 177 | at: workspace 178 | 179 | - run: 180 | name: Create virtual-environment 181 | command: | 182 | python3 -m venv venv 183 | . venv/bin/activate 184 | 185 | - run: 186 | name: Install from pip 187 | # Try to install the same version from PyPI 188 | command: | 189 | . venv/bin/activate 190 | pip install flask-log-request-id==`cat workspace/version.txt` 191 | pip freeze | grep -i flask-log-request-id 192 | 193 | workflows: 194 | version: 2 195 | test_build_deploy: 196 | jobs: 197 | - test-3.6 198 | - test-3.5 199 | - build: 200 | requires: 201 | - test-3.6 202 | - test-3.5 203 | 204 | - deploy_test_pypi: 205 | filters: 206 | branches: 207 | only: 208 | - staging 209 | requires: 210 | - build 211 | 212 | - install_from_test_pypi: 213 | requires: 214 | - deploy_test_pypi 215 | 216 | - request_permission_for_public_pypi: 217 | type: approval 218 | requires: 219 | - build 220 | filters: 221 | tags: 222 | only: 223 | - /^v[\d\.]+$/ 224 | branches: 225 | only: master 226 | 227 | - deploy_pypi: 228 | requires: 229 | - request_permission_for_public_pypi 230 | 231 | - install_from_pypi: 232 | requires: 233 | - deploy_pypi 234 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # Jupyter Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # SageMath parsed files 79 | *.sage.py 80 | 81 | # Environments 82 | .env 83 | .venv 84 | env/ 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | .spyproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | 95 | # mkdocs documentation 96 | /site 97 | 98 | # mypy 99 | .mypy_cache/ 100 | 101 | # Pycharm 102 | .idea/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017-present Workable SA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Flask-Log-Request-Id 3 | 4 | [![CircleCI](https://img.shields.io/circleci/project/github/Workable/flask-log-request-id.svg)](https://circleci.com/gh/Workable/flask-log-request-id) 5 | 6 | **Flask-Log-Request-Id** is an extension for [Flask](http://flask.pocoo.org/) that can parse and handle the 7 | request-id sent by request processors like [Amazon ELB](http://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-request-tracing.html), 8 | [Heroku Request-ID](https://devcenter.heroku.com/articles/http-request-id) or any multi-tier infrastructure as the one used 9 | at microservices. A common usage scenario is to inject the request_id in the logging system so that all log records, 10 | even those emitted by third party libraries, have attached the request_id that initiated their call. This can 11 | greatly improve tracing and debugging of problems. 12 | 13 | ## Installation 14 | 15 | The easiest way to install it is using ``pip`` from PyPI 16 | 17 | ```bash 18 | pip install flask-log-request-id 19 | ``` 20 | 21 | ## Usage 22 | 23 | Flask-Log-Request-Id provides the `current_request_id()` function which can be used at any time to get the request 24 | id of the initiated execution chain. 25 | 26 | 27 | ### Example 1: Parse request id and print to stdout 28 | ```python 29 | from flask_log_request_id import RequestID, current_request_id 30 | 31 | [...] 32 | 33 | RequestID(app) 34 | 35 | 36 | @app.route('/') 37 | def hello(): 38 | print('Current request id: {}'.format(current_request_id())) 39 | ``` 40 | 41 | 42 | ### Example 2: Parse request id and send it to to logging 43 | 44 | In the following example, we will use the `RequestIDLogFilter` to inject the request id on all log events, and 45 | a custom formatter to print this information. If all these sounds unfamiliar please take a look at [python's logging 46 | system](https://docs.python.org/3/library/logging.html) 47 | 48 | 49 | ```python 50 | import logging 51 | import logging.config 52 | from random import randint 53 | from flask import Flask 54 | from flask_log_request_id import RequestID, RequestIDLogFilter 55 | 56 | def generic_add(a, b): 57 | """Simple function to add two numbers that is not aware of the request id""" 58 | logging.debug('Called generic_add({}, {})'.format(a, b)) 59 | return a + b 60 | 61 | app = Flask(__name__) 62 | RequestID(app) 63 | 64 | # Setup logging 65 | handler = logging.StreamHandler() 66 | handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - level=%(levelname)s - request_id=%(request_id)s - %(message)s")) 67 | handler.addFilter(RequestIDLogFilter()) # << Add request id contextual filter 68 | logging.getLogger().addHandler(handler) 69 | 70 | 71 | @app.route('/') 72 | def index(): 73 | a, b = randint(1, 15), randint(1, 15) 74 | logging.info('Adding two random numbers {} {}'.format(a, b)) 75 | return str(generic_add(a, b)) 76 | ``` 77 | 78 | The above will output the following log entries: 79 | 80 | ``` 81 | 2017-07-25 16:15:25,912 - __main__ - level=INFO - request_id=7ff2946c-efe0-4c51-b337-fcdcdfe8397b - Adding two random numbers 11 14 82 | 2017-07-25 16:15:25,913 - __main__ - level=DEBUG - request_id=7ff2946c-efe0-4c51-b337-fcdcdfe8397b - Called generic_add(11, 14) 83 | 2017-07-25 16:15:25,913 - werkzeug - level=INFO - request_id=None - 127.0.0.1 - - [25/Jul/2017 16:15:25] "GET / HTTP/1.1" 200 - 84 | ``` 85 | 86 | ### Example 3: Forward request_id to celery tasks 87 | 88 | Flask-Log-Request-Id comes with extras to forward the context of current request_id to the workers of celery tasks. 89 | In order to use this feature you need to enable the celery plugin and configure the `Celery` instance. Then you can 90 | use `current_request_id()` from inside your worker 91 | 92 | ```python 93 | from flask_log_request_id.extras.celery import enable_request_id_propagation 94 | from flask_log_request_id import current_request_id 95 | from celery.app import Celery 96 | import logging 97 | 98 | celery = Celery() 99 | enable_request_id_propagation(celery) # << This step here is critical to propagate request-id to workers 100 | 101 | app = Flask() 102 | 103 | @celery.task() 104 | def generic_add(a, b): 105 | """Simple function to add two numbers that is not aware of the request id""" 106 | 107 | logging.debug('Called generic_add({}, {}) from request_id: {}'.format(a, b, current_request_id())) 108 | return a + b 109 | 110 | @app.route('/') 111 | def index(): 112 | a, b = randint(1, 15), randint(1, 15) 113 | logging.info('Adding two random numbers {} {}'.format(a, b)) 114 | return str(generic_add.delay(a, b)) # Calling the task here, will forward the request id to the workers 115 | ``` 116 | 117 | You can follow the same logging strategy for both web application and workers using the `RequestIDLogFilter` as shown in 118 | example 1 and 2. 119 | 120 | ### Example 4: If you want to return request id in response 121 | 122 | This will be useful while integrating with frontend where in you can get the request id from the response (be it 400 or 500) and then trace the request in logs. 123 | 124 | ```python 125 | from flask_log_request_id import current_request_id 126 | 127 | @app.after_request 128 | def append_request_id(response): 129 | response.headers.add('X-REQUEST-ID', current_request_id()) 130 | return response 131 | ``` 132 | 133 | ## Configuration 134 | 135 | The following parameters can be configured through Flask's configuration system: 136 | 137 | | Configuration Name | Description | 138 | | ------------------ | ----------- | 139 | | **LOG_REQUEST_ID_GENERATE_IF_NOT_FOUND**| In case the request does not hold any request id, the extension will generate one. Otherwise `current_request_id` will return None. | 140 | | **LOG_REQUEST_ID_LOG_ALL_REQUESTS** | If True, it will emit a log event at the request containing all the details as `werkzeug` would done along with the `request_id` . | 141 | | **LOG_REQUEST_ID_G_OBJECT_ATTRIBUTE** | This is the attribute of `Flask.g` object to store the current request id. Should be changed only if there is a problem. Use `current_request_id()` to fetch the current id. | 142 | 143 | 144 | ## License 145 | 146 | See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT). 147 | -------------------------------------------------------------------------------- /examples/server_with_celery/Readme.md: -------------------------------------------------------------------------------- 1 | ## Web Application that delegate tasks to remote workers 2 | 3 | To run the example you need to: 4 | ### Step 1: Run celery workers 5 | Make sure you have a celery compatible environment with celery package installed and a working rabbit mq or any other 6 | message server. 7 | 8 | ```sh 9 | cd server_with_celery 10 | celery worker -A worker 11 | ``` 12 | 13 | ### Step 2: Run web application 14 | 15 | ```sh 16 | cd server_with_celery 17 | python web.py 18 | ``` -------------------------------------------------------------------------------- /examples/server_with_celery/example_app/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from random import randint 3 | from flask import Flask 4 | from flask_log_request_id import RequestID, RequestIDLogFilter 5 | 6 | from .celery import celery 7 | from . import tasks 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def initialize_logging(): 13 | # Setup logging 14 | handler = logging.StreamHandler() 15 | handler.setFormatter( 16 | logging.Formatter("%(asctime)s - %(name)s - level=%(levelname)s - request_id=%(request_id)s - %(message)s")) 17 | handler.addFilter(RequestIDLogFilter()) # << Add request id contextual filter 18 | logging.getLogger().addHandler(handler) 19 | logging.getLogger().setLevel(logging.DEBUG) 20 | 21 | 22 | app = Flask(__name__) 23 | app.config['LOG_REQUEST_ID_LOG_ALL_REQUESTS'] = True 24 | RequestID(app) 25 | initialize_logging() 26 | 27 | 28 | @ app.route('/') 29 | def index(): 30 | a, b = randint(1, 15), randint(1, 15) 31 | logger.info('Adding two random numbers {} {}'.format(a, b)) 32 | task = tasks.generic_add.delay(a, b) 33 | 34 | return "Task {!s} started".format(task) 35 | -------------------------------------------------------------------------------- /examples/server_with_celery/example_app/celery.py: -------------------------------------------------------------------------------- 1 | from celery import Celery, signals 2 | from flask_log_request_id.extras.celery import enable_request_id_propagation 3 | 4 | celery = Celery() 5 | 6 | # You need to enable propagation on celery application 7 | enable_request_id_propagation(celery) 8 | 9 | -------------------------------------------------------------------------------- /examples/server_with_celery/example_app/tasks.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .celery import celery 3 | 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | @celery.task() 9 | def generic_add(a, b): 10 | print 11 | """Simple function to add two numbers""" 12 | logger.info('Called generic_add({}, {})'.format(a, b)) 13 | return a + b 14 | -------------------------------------------------------------------------------- /examples/server_with_celery/web.py: -------------------------------------------------------------------------------- 1 | from example_app import app 2 | 3 | 4 | if __name__ == '__main__': 5 | app.run(debug=True) 6 | -------------------------------------------------------------------------------- /examples/server_with_celery/worker.py: -------------------------------------------------------------------------------- 1 | import logging.config 2 | from example_app import tasks, celery 3 | from flask_log_request_id import RequestIDLogFilter 4 | 5 | 6 | def initialize_celery_logging(): 7 | LOGGING = { 8 | 'version': 1, 9 | 'disable_existing_loggers': False, 10 | 'filters': { 11 | 'request_id_filter': { 12 | '()': RequestIDLogFilter 13 | }, 14 | }, 15 | 'formatters': { 16 | 'simple_request_id': { 17 | 'format': "{asctime} - {name} - level={levelname} - request_id={request_id} - {message}", 18 | 'style': '{' 19 | } 20 | }, 21 | 'handlers': { 22 | 'console': { 23 | 'class': 'logging.StreamHandler', 24 | 'level': 'DEBUG', 25 | 'formatter': 'simple_request_id', 26 | 'filters': ['request_id_filter'] 27 | } 28 | }, 29 | 'loggers': { 30 | 'example_app': { 31 | 'level': 'DEBUG', 32 | 'handlers': ['console'], 33 | }, 34 | }, 35 | 'root': { 36 | 'level': 'DEBUG', 37 | 'handlers': ['console'] 38 | } 39 | } 40 | logging.config.dictConfig(LOGGING) 41 | 42 | initialize_celery_logging() 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/server_with_logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from random import randint 3 | from flask import Flask 4 | from flask_log_request_id import RequestID, RequestIDLogFilter, current_request_id 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | def generic_add(a, b): 10 | """Simple function to add two numbers""" 11 | logger.debug('Called generic_add({}, {})'.format(a, b)) 12 | return a + b 13 | 14 | 15 | def initialize_logging(): 16 | # Setup logging 17 | handler = logging.StreamHandler() 18 | handler.setFormatter( 19 | logging.Formatter("%(asctime)s - %(name)s - level=%(levelname)s - request_id=%(request_id)s - %(message)s")) 20 | handler.addFilter(RequestIDLogFilter()) # << Add request id contextual filter 21 | logging.getLogger().addHandler(handler) 22 | logging.getLogger().setLevel(logging.DEBUG) 23 | 24 | 25 | app = Flask(__name__) 26 | app.config['LOG_REQUEST_ID_LOG_ALL_REQUESTS'] = True 27 | RequestID(app) 28 | initialize_logging() 29 | 30 | 31 | @app.route('/') 32 | def index(): 33 | a, b = randint(1, 15), randint(1, 15) 34 | logger.info('Adding two random numbers {} {}'.format(a, b)) 35 | return str(generic_add(a, b)) 36 | 37 | 38 | 39 | if __name__ == '__main__': 40 | app.run(debug=True) -------------------------------------------------------------------------------- /flask_log_request_id/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .request_id import RequestID, current_request_id 3 | from .filters import RequestIDLogFilter 4 | from . import parser 5 | 6 | 7 | __version__ = '0.0.0-dev' 8 | 9 | 10 | __all__ = [ 11 | 'RequestID', 12 | 'current_request_id', 13 | 'RequestIDLogFilter', 14 | 'parser' 15 | ] 16 | -------------------------------------------------------------------------------- /flask_log_request_id/ctx_fetcher.py: -------------------------------------------------------------------------------- 1 | class ExecutedOutsideContext(Exception): 2 | """ 3 | Exception to be raised if a fetcher was called outside its context 4 | """ 5 | pass 6 | 7 | 8 | class MultiContextRequestIdFetcher(object): 9 | """ 10 | A callable that can fetch request id from different context as Flask, Celery etc. 11 | """ 12 | 13 | def __init__(self): 14 | """ 15 | Initialize 16 | """ 17 | self.ctx_fetchers = [] 18 | 19 | def __call__(self): 20 | 21 | for ctx_fetcher in self.ctx_fetchers: 22 | try: 23 | return ctx_fetcher() 24 | except ExecutedOutsideContext: 25 | continue 26 | return None 27 | 28 | def register_fetcher(self, ctx_fetcher): 29 | """ 30 | Register another context-specialized fetcher 31 | :param Callable ctx_fetcher: A callable that will return the id or raise ExecutedOutsideContext if it was 32 | executed outside its context 33 | """ 34 | if ctx_fetcher not in self.ctx_fetchers: 35 | self.ctx_fetchers.append(ctx_fetcher) 36 | -------------------------------------------------------------------------------- /flask_log_request_id/extras/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workable/flask-log-request-id/b21fe7bcff371b082a257ccf1d077d8b8279f169/flask_log_request_id/extras/__init__.py -------------------------------------------------------------------------------- /flask_log_request_id/extras/celery.py: -------------------------------------------------------------------------------- 1 | from celery import current_task, signals 2 | import logging as _logging 3 | 4 | from ..request_id import current_request_id 5 | from ..ctx_fetcher import ExecutedOutsideContext 6 | 7 | 8 | _CELERY_X_HEADER = 'x_request_id' 9 | logger = _logging.getLogger(__name__) 10 | 11 | 12 | def enable_request_id_propagation(celery_app): 13 | """ 14 | Will attach signal on celery application in order to propagate 15 | current request id to workers 16 | :param celery_app: The celery application 17 | """ 18 | signals.before_task_publish.connect(on_before_publish_insert_request_id_header) 19 | 20 | 21 | def on_before_publish_insert_request_id_header(headers, **kwargs): 22 | """ 23 | This function is meant to be used as signal processor for "before_task_publish". 24 | :param Dict headers: The headers of the message 25 | :param kwargs: Any extra keyword arguments 26 | """ 27 | if _CELERY_X_HEADER not in headers: 28 | request_id = current_request_id() 29 | headers[_CELERY_X_HEADER] = request_id 30 | logger.debug("Forwarding request_id '{}' to the task consumer.".format(request_id)) 31 | 32 | 33 | def ctx_celery_task_get_request_id(): 34 | """ 35 | Fetch the request id from the headers of the current celery task. 36 | """ 37 | if current_task._get_current_object() is None: 38 | raise ExecutedOutsideContext() 39 | 40 | return current_task.request.get(_CELERY_X_HEADER, None) 41 | 42 | 43 | # If you import this module then you are interested for this context 44 | current_request_id.register_fetcher(ctx_celery_task_get_request_id) 45 | -------------------------------------------------------------------------------- /flask_log_request_id/filters.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .request_id import current_request_id 3 | 4 | 5 | class RequestIDLogFilter(logging.Filter): 6 | """ 7 | Log filter to inject the current request id of the request under `log_record.request_id` 8 | """ 9 | 10 | def filter(self, log_record): 11 | log_record.request_id = current_request_id() 12 | return log_record 13 | -------------------------------------------------------------------------------- /flask_log_request_id/parser.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | 3 | 4 | def amazon_elb_trace_id(): 5 | """ 6 | Get the amazon ELB trace id from current Flask request context 7 | :return: The found Trace-ID or None if not found 8 | :rtype: str | None 9 | """ 10 | amazon_request_id = request.headers.get('X-Amzn-Trace-Id', '') 11 | trace_id_params = dict(x.split('=') if '=' in x else (x, None) for x in amazon_request_id.split(';')) 12 | if 'Self' in trace_id_params: 13 | return trace_id_params['Self'] 14 | if 'Root' in trace_id_params: 15 | return trace_id_params['Root'] 16 | 17 | return None 18 | 19 | 20 | def generic_http_header_parser_for(header_name): 21 | """ 22 | A parser factory to extract the request id from an HTTP header 23 | :return: A parser that can be used to extract the request id from the current request context 24 | :rtype: ()->str|None 25 | """ 26 | 27 | def parser(): 28 | request_id = request.headers.get(header_name, '').strip() 29 | 30 | if not request_id: 31 | # If the request id is empty return None 32 | return None 33 | return request_id 34 | return parser 35 | 36 | 37 | def x_request_id(): 38 | """ 39 | Parser for generic X-Request-ID header 40 | :rtype: str|None 41 | """ 42 | return generic_http_header_parser_for('X-Request-ID')() 43 | 44 | 45 | def x_correlation_id(): 46 | """ 47 | Parser for generic X-Correlation-ID header 48 | :rtype: str|None 49 | """ 50 | return generic_http_header_parser_for('X-Correlation-ID')() 51 | 52 | 53 | def auto_parser(parsers=(x_request_id, x_correlation_id, amazon_elb_trace_id)): 54 | """ 55 | Meta parser that will try all known parser and it will bring the first found id 56 | :param list[Callable] parsers: A list of callable parsers to try to extract request_id 57 | :return: The request id if it is found or None 58 | :rtype: str|None 59 | """ 60 | 61 | for parser in parsers: 62 | request_id = parser() 63 | if request_id is not None: 64 | return request_id 65 | 66 | return None # request-id was not found 67 | -------------------------------------------------------------------------------- /flask_log_request_id/request_id.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import logging as _logging 3 | 4 | from flask import request, g, current_app 5 | 6 | from .parser import auto_parser 7 | from .ctx_fetcher import MultiContextRequestIdFetcher, ExecutedOutsideContext 8 | 9 | 10 | logger = _logging.getLogger(__name__) 11 | 12 | 13 | def flask_ctx_get_request_id(): 14 | """ 15 | Get request id from flask's G object 16 | :return: The id or None if not found. 17 | """ 18 | from flask import _app_ctx_stack as stack # We do not support < Flask 0.9 19 | 20 | if stack.top is None: 21 | raise ExecutedOutsideContext() 22 | 23 | g_object_attr = stack.top.app.config['LOG_REQUEST_ID_G_OBJECT_ATTRIBUTE'] 24 | return g.get(g_object_attr, None) 25 | 26 | 27 | current_request_id = MultiContextRequestIdFetcher() 28 | current_request_id.register_fetcher(flask_ctx_get_request_id) 29 | 30 | 31 | class RequestID(object): 32 | """ 33 | Flask extension to parse or generate the id of each request 34 | """ 35 | 36 | def __init__(self, app=None, request_id_parser=None, request_id_generator=None): 37 | """ 38 | Initialize extension 39 | :param flask.Application | None app: The flask application or None if you want to initialize later 40 | :param None | () -> str request_id_parser: The parser to extract request-id from request headers. If None 41 | the default auto_parser() will be used that will try all known parsers. 42 | :param ()->str request_id_generator: A callable to use in case of missing request-id. 43 | """ 44 | self.app = app 45 | self._request_id = None 46 | 47 | self._request_id_parser = request_id_parser 48 | if self._request_id_parser is None: 49 | self._request_id_parser = auto_parser 50 | 51 | self._request_id_generator = request_id_generator 52 | if self._request_id_generator is None: 53 | self._request_id_generator = lambda: str(uuid.uuid4()) 54 | 55 | self._generate_id_if_not_found = True 56 | 57 | if app is not None: 58 | self.init_app(app) 59 | 60 | def init_app(self, app): 61 | 62 | # Default configuration 63 | app.config.setdefault('LOG_REQUEST_ID_GENERATE_IF_NOT_FOUND', True) 64 | app.config.setdefault('LOG_REQUEST_ID_LOG_ALL_REQUESTS', False) 65 | app.config.setdefault('LOG_REQUEST_ID_G_OBJECT_ATTRIBUTE', 'log_request_id') 66 | 67 | # Register before request callback 68 | @app.before_request 69 | def _persist_request_id(): 70 | """ 71 | It will parse and persist the RequestID from the HTTP request. If not 72 | found it will generate a new one if requestsed. 73 | 74 | To be used as a consumer of Flask.before_request event. 75 | """ 76 | g_object_attr = current_app.config['LOG_REQUEST_ID_G_OBJECT_ATTRIBUTE'] 77 | 78 | setattr(g, g_object_attr, self._request_id_parser()) 79 | if g.get(g_object_attr) is None: 80 | if app.config['LOG_REQUEST_ID_GENERATE_IF_NOT_FOUND']: 81 | setattr(g, g_object_attr, self._request_id_generator()) 82 | 83 | # Register after request 84 | if app.config['LOG_REQUEST_ID_LOG_ALL_REQUESTS']: 85 | app.after_request(self._log_http_event) 86 | 87 | @staticmethod 88 | def _log_http_event(response): 89 | """ 90 | It will create a log event as werkzeug but at the end of request holding the request-id 91 | 92 | Intended usage is a handler of Flask.after_request 93 | :return: The same response object 94 | """ 95 | logger.info( 96 | '{ip} - - "{method} {path} {status_code}"'.format( 97 | ip=request.remote_addr, 98 | method=request.method, 99 | path=request.path, 100 | status_code=response.status_code) 101 | ) 102 | return response 103 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=1 3 | detailed-errors=1 4 | with-coverage=1 5 | cover-branches=1 6 | cover-xml=1 7 | cover-package=flask_log_request_id 8 | with-xunit=1 9 | 10 | [flake8] 11 | max-line-length=120 12 | exclude = 13 | .git, 14 | __pycache__, 15 | build, 16 | dist -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flask-Log-Request-Id 3 | ==================== 4 | 5 | |CircleCI| 6 | 7 | **Flask-Log-Request-Id** is an extension for `Flask`_ that can parse and handle 8 | the request-id sent by request processors like `Amazon ELB`_, `Heroku Request-ID`_ 9 | or any multi-tier infrastructure as the one used at microservices. A common 10 | usage scenario is to inject the request\\_id in the logging system so that all 11 | log records, even those emitted by third party libraries, have attached the 12 | request\\_id that initiated their call. This can greatly improve tracing and debugging of problems. 13 | 14 | 15 | Features 16 | -------- 17 | 18 | Flask-Log-Request-Id provides the ``current_request_id()`` function which can be used 19 | at any time to get the request id of the initiated execution chain. It also comes with 20 | log filter to inject this information on log events as also an extension to forward 21 | the current request id into Celery's workers. 22 | 23 | 24 | Example: Parse request id and send it to to logging 25 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | 27 | In the following example, we will use the ``RequestIDLogFilter`` to inject 28 | the request id on all log events, and a custom formatter to print this 29 | information. If all these sounds unfamiliar please take a look at `python logging system`_ 30 | 31 | .. code:: python 32 | 33 | import logging 34 | import logging.config 35 | from random import randint 36 | from flask import Flask 37 | from flask_log_request_id import RequestID, RequestIDLogFilter 38 | 39 | def generic_add(a, b): 40 | \"""Simple function to add two numbers that is not aware of the request id\""" 41 | logging.debug('Called generic_add({}, {})'.format(a, b)) 42 | return a + b 43 | 44 | app = Flask(__name__) 45 | RequestID(app) 46 | 47 | # Setup logging 48 | handler = logging.StreamHandler() 49 | handler.setFormatter( 50 | logging.Formatter("%(asctime)s - %(name)s - level=%(levelname)s - request_id=%(request_id)s - %(message)s")) 51 | handler.addFilter(RequestIDLogFilter()) # << Add request id contextual filter 52 | logging.getLogger().addHandler(handler) 53 | 54 | 55 | @app.route('/') 56 | def index(): 57 | a, b = randint(1, 15), randint(1, 15) 58 | logging.info('Adding two random numbers {} {}'.format(a, b)) 59 | return str(generic_add(a, b)) 60 | 61 | 62 | Installation 63 | ------------ 64 | The easiest way to install it is using ``pip`` from PyPI 65 | 66 | .. code:: bash 67 | 68 | pip install flask-log-request-id 69 | 70 | 71 | License 72 | ------- 73 | 74 | See the `LICENSE`_ file for license rights and limitations (MIT). 75 | 76 | 77 | .. _Flask: http://flask.pocoo.org/ 78 | .. _Amazon ELB: http://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-request-tracing.html 79 | .. _Heroku Request-ID: https://devcenter.heroku.com/articles/http-request-id 80 | .. _python logging system: https://docs.python.org/3/library/logging.html 81 | .. _LICENSE: https://github.com/Workable/flask-log-request-id/blob/master/LICENSE.md 82 | .. |CircleCI| image:: https://img.shields.io/circleci/project/github/Workable/flask-log-request-id.svg 83 | :target: https://circleci.com/gh/Workable/flask-log-request-id 84 | 85 | """ 86 | import re 87 | import ast 88 | from setuptools import setup 89 | 90 | _version_re = re.compile(r'__version__\s+=\s+(.*)') 91 | 92 | with open('flask_log_request_id/__init__.py', 'rb') as f: 93 | version = str(ast.literal_eval(_version_re.search( 94 | f.read().decode('utf-8')).group(1))) 95 | 96 | test_requirements = [ 97 | 'nose', 98 | 'flake8', 99 | 'mock==2.0.0', 100 | 'coverage~=4.5.4', 101 | 'celery~=4.3.0' 102 | ] 103 | 104 | setup( 105 | name='Flask-Log-Request-ID', 106 | version=version, 107 | url='http://github.com/Workable/flask-log-request-id', 108 | license='MIT', 109 | author='Konstantinos Paliouras, Ioannis Foukarakis', 110 | author_email='squarious@gmail.com, ioannis.foukarakis@gmail.com', 111 | description='Flask extension that can parse and handle multiple types of request-id ' 112 | 'sent by request processors like Amazon ELB, Heroku or any multi-tier ' 113 | 'infrastructure as the one used for microservices.', 114 | long_description=__doc__, 115 | maintainer="Konstantinos Paliouras", 116 | maintainer_email="squarious@gmail.com", 117 | packages=[ 118 | 'flask_log_request_id', 119 | 'flask_log_request_id.extras'], 120 | zip_safe=False, 121 | include_package_data=True, 122 | platforms='any', 123 | install_requires=[ 124 | 'Flask>=0.8', 125 | ], 126 | tests_require=test_requirements, 127 | setup_requires=[ 128 | "flake8", 129 | "nose" 130 | ], 131 | extras_require={ 132 | 'test': test_requirements 133 | }, 134 | test_suite='nose.collector', 135 | classifiers=[ 136 | 'Environment :: Web Environment', 'Intended Audience :: Developers', 137 | 'License :: OSI Approved :: MIT License', 138 | 'Operating System :: OS Independent', 'Programming Language :: Python', 139 | 'Programming Language :: Python :: 3', 140 | 'Topic :: Software Development :: Libraries :: Python Modules' 141 | ]) 142 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workable/flask-log-request-id/b21fe7bcff371b082a257ccf1d077d8b8279f169/tests/__init__.py -------------------------------------------------------------------------------- /tests/ctx_fetcher_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import mock 3 | from flask_log_request_id.ctx_fetcher import MultiContextRequestIdFetcher, ExecutedOutsideContext 4 | 5 | 6 | class CtxFetcherTestCase(unittest.TestCase): 7 | 8 | def test_empty_multictx_fetcher(self): 9 | fetcher = MultiContextRequestIdFetcher() 10 | 11 | self.assertIsNone(fetcher()) 12 | 13 | def test_register_ctx_fetcher(self): 14 | fetcher = MultiContextRequestIdFetcher() 15 | 16 | ctx_fetcher = mock.Mock() 17 | 18 | # One fetcher in context 19 | ctx_fetcher.return_value = 'response' 20 | fetcher.register_fetcher(ctx_fetcher) 21 | self.assertEqual(fetcher(), 'response') 22 | 23 | # One fetcher outside context 24 | ctx_fetcher.side_effect = ExecutedOutsideContext 25 | self.assertIsNone(fetcher()) 26 | 27 | def test_register_multiple_fetcher(self): 28 | multi_fetcher = MultiContextRequestIdFetcher() 29 | 30 | ctx_fetchers = [ 31 | mock.Mock(), 32 | mock.Mock()] 33 | for index, f in enumerate(ctx_fetchers): 34 | multi_fetcher.register_fetcher(f) 35 | f.return_value = "fetcher:{}".format(index) 36 | 37 | # Second is in context 38 | ctx_fetchers[0].side_effect = ExecutedOutsideContext 39 | self.assertEqual(multi_fetcher(), 'fetcher:1') 40 | 41 | # None is in context 42 | ctx_fetchers[1].side_effect = ExecutedOutsideContext 43 | self.assertIsNone(multi_fetcher()) 44 | 45 | # Both in context 46 | ctx_fetchers[0].side_effect = None 47 | ctx_fetchers[1].side_effect = None 48 | self.assertEqual(multi_fetcher(), 'fetcher:0') 49 | 50 | def test_register_same_fetcher(self): 51 | multi_fetcher = MultiContextRequestIdFetcher() 52 | 53 | fetcher1 = mock.Mock() 54 | multi_fetcher.register_fetcher(fetcher1) 55 | self.assertEqual( 56 | multi_fetcher.ctx_fetchers, 57 | [fetcher1] 58 | ) 59 | 60 | # Re-register 61 | multi_fetcher.register_fetcher(fetcher1) 62 | self.assertEqual( 63 | multi_fetcher.ctx_fetchers, 64 | [fetcher1] 65 | ) 66 | 67 | 68 | 69 | if __name__ == '__main__': 70 | unittest.main() 71 | -------------------------------------------------------------------------------- /tests/extras/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workable/flask-log-request-id/b21fe7bcff371b082a257ccf1d077d8b8279f169/tests/extras/__init__.py -------------------------------------------------------------------------------- /tests/extras/celery_tests.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import unittest 3 | 4 | from celery import Celery 5 | from flask_log_request_id.extras.celery import (ExecutedOutsideContext, 6 | on_before_publish_insert_request_id_header, 7 | ctx_celery_task_get_request_id) 8 | 9 | 10 | class MockedTask(object): 11 | 12 | def __init__(self): 13 | self.apply_async_called = None 14 | 15 | def apply_async(self, *args, **kwargs): 16 | self.apply_async_called = { 17 | 'args': args, 18 | 'kwargs': kwargs 19 | } 20 | 21 | 22 | class CeleryIntegrationTestCase(unittest.TestCase): 23 | 24 | @mock.patch('flask_log_request_id.extras.celery.current_request_id') 25 | def test_enable_request_id_propagation(self, mocked_current_request_id): 26 | mocked_current_request_id.return_value = 15 27 | 28 | headers = {} 29 | on_before_publish_insert_request_id_header(headers=headers) 30 | self.assertDictEqual( 31 | { 32 | 'x_request_id': 15 33 | }, 34 | headers) 35 | 36 | @mock.patch('flask_log_request_id.extras.celery.current_task') 37 | def test_ctx_fetcher_outside_context(self, mocked_current_task): 38 | mocked_current_task._get_current_object.return_value = None 39 | with self.assertRaises(ExecutedOutsideContext): 40 | ctx_celery_task_get_request_id() 41 | 42 | @mock.patch('flask_log_request_id.extras.celery.current_task') 43 | def test_ctx_fetcher_inside_context(self, mocked_current_task): 44 | mocked_current_task._get_current_object.return_value = True 45 | mocked_current_task.request.get.side_effect = lambda a, default: {'x_request_id': 15, 'other': 'bar'}[a] 46 | 47 | self.assertEqual(ctx_celery_task_get_request_id(), 15) 48 | 49 | 50 | if __name__ == '__main__': 51 | unittest.main() 52 | -------------------------------------------------------------------------------- /tests/parser_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from flask import Flask 4 | 5 | from flask_log_request_id.parser import amazon_elb_trace_id, x_correlation_id, x_request_id, auto_parser 6 | 7 | 8 | class AmazonELBTraceIdTestCase(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self.app = Flask(__name__) 12 | 13 | def test_with_header_only_root(self): 14 | with self.app.test_request_context(headers={'X-Amzn-Trace-Id': 'Root=1-67891233-abc'}): 15 | self.assertEqual('1-67891233-abc', amazon_elb_trace_id()) 16 | 17 | def test_with_header_root_and_self(self): 18 | with self.app.test_request_context(headers={'X-Amzn-Trace-Id': 'Self=1-67891234-def;Root=1-67891233-abc'}): 19 | self.assertEqual('1-67891234-def', amazon_elb_trace_id()) 20 | 21 | def test_with_header_root_self_and_custom_params(self): 22 | with self.app.test_request_context(headers={'X-Amzn-Trace-Id': 'Self=1-def;Root=1-abc;CalledFrom=app'}): 23 | self.assertEqual('1-def', amazon_elb_trace_id()) 24 | 25 | def test_without_header(self): 26 | with self.app.test_request_context(): 27 | self.assertIsNone(amazon_elb_trace_id()) 28 | 29 | def test_with_invalid_header(self): 30 | with self.app.test_request_context(headers={'X-Amzn-Trace-Id': 'ohmythisisnotvalid'}): 31 | self.assertIsNone(amazon_elb_trace_id()) 32 | 33 | 34 | class RequestIDIdTestCase(unittest.TestCase): 35 | 36 | def setUp(self): 37 | self.app = Flask(__name__) 38 | 39 | def test_x_request_id_empty_header(self): 40 | 41 | with self.app.test_request_context(): 42 | # No header should return empty 43 | self.assertIsNone(x_request_id()) 44 | 45 | def test_x_request_id_invalid_header(self): 46 | with self.app.test_request_context(headers={'X-Request-ID': ''}): 47 | # Wrong header 48 | self.assertIsNone(x_request_id()) 49 | 50 | def test_x_request_id_valid_header(self): 51 | with self.app.test_request_context(headers={'X-Request-ID': '1-67891233-root'}): 52 | # Simple request id 53 | self.assertEqual('1-67891233-root', x_request_id()) 54 | 55 | def test_x_request_id_wrong_sensitivity(self): 56 | with self.app.test_request_context(headers={'x-request-id': '1-67891233-root'}): 57 | # Simple request with case insensitivity 58 | self.assertEqual('1-67891233-root', x_request_id()) 59 | 60 | 61 | class CorrelationIDIdTestCase(unittest.TestCase): 62 | 63 | def setUp(self): 64 | self.app = Flask(__name__) 65 | 66 | def test_x_correlation_id_empty_header(self): 67 | 68 | with self.app.test_request_context(): 69 | # No header should return empty 70 | self.assertIsNone(x_correlation_id()) 71 | 72 | def test_x_correlation_id_invalid_header(self): 73 | with self.app.test_request_context(headers={'X-Correlation-ID': ''}): 74 | # Wrong header 75 | self.assertIsNone(x_correlation_id()) 76 | 77 | def test_x_correlation_id_valid_header(self): 78 | with self.app.test_request_context(headers={'X-Correlation-ID': '1-67891233-root'}): 79 | # Simple request id 80 | self.assertEqual('1-67891233-root', x_correlation_id()) 81 | 82 | def test_x_correlation_id_wrong_sensitivity(self): 83 | with self.app.test_request_context(headers={'x-correlation-id': '1-67891233-root'}): 84 | # Simple request with case insensitivity 85 | self.assertEqual('1-67891233-root', x_correlation_id()) 86 | 87 | 88 | class AutoParserTestCase(unittest.TestCase): 89 | 90 | def setUp(self): 91 | self.app = Flask(__name__) 92 | 93 | def test_auto_parser_empty_header(self): 94 | 95 | with self.app.test_request_context(): 96 | # No header should return empty 97 | self.assertIsNone(auto_parser()) 98 | 99 | def test_auto_parser_invalid_header(self): 100 | with self.app.test_request_context(headers={ 101 | 'X-Amzn-Trace-Id': '', 102 | 'X-Correlation-ID': '', 103 | 'X-Request-ID': '' 104 | }): 105 | # Wrong headers 106 | self.assertIsNone(auto_parser()) 107 | 108 | def test_auto_parser_precedence_1(self): 109 | with self.app.test_request_context(headers={ 110 | 'X-Amzn-Trace-Id': 'Root=1-67891233-root', 111 | 'X-Correlation-ID': '1-67891233-correlation', 112 | 'X-Request-ID': '1-67891233-request-id' 113 | }): 114 | # Precedence scenario 1 115 | self.assertEqual('1-67891233-request-id', auto_parser()) 116 | 117 | def test_auto_parser_precedence_2(self): 118 | with self.app.test_request_context(headers={ 119 | 'X-Amzn-Trace-Id': 'Root=1-67891233-root', 120 | 'X-Correlation-ID': '1-67891233-correlation', 121 | }): 122 | # Precedence scenario 1 123 | self.assertEqual('1-67891233-correlation', auto_parser()) 124 | 125 | 126 | if __name__ == '__main__': 127 | unittest.main() 128 | -------------------------------------------------------------------------------- /tests/request_id_tests.py: -------------------------------------------------------------------------------- 1 | import flask 2 | import unittest 3 | 4 | from flask_log_request_id.request_id import RequestID, current_request_id 5 | from mock import patch 6 | 7 | 8 | class RequestIDTestCase(unittest.TestCase): 9 | def setUp(self): 10 | self.app = flask.Flask(__name__) 11 | 12 | self.app.route('/')(lambda: 'hello world') 13 | self.app.testing = True 14 | 15 | def test_lazy_initialization(self): 16 | # Bug #38: https://github.com/Workable/flask-log-request-id/issues/38 17 | request_id = RequestID() 18 | request_id.init_app(self.app) 19 | with self.app.test_request_context(headers={'X-Amzn-Trace-Id': 'Self=1-67891234-def;Root=1-67891233-abc'}): 20 | self.app.preprocess_request() 21 | self.assertEqual('1-67891234-def', current_request_id()) 22 | 23 | def test_default_request_id_parser_with_amazon(self): 24 | RequestID(self.app) 25 | with self.app.test_request_context(headers={'X-Amzn-Trace-Id': 'Self=1-67891234-def;Root=1-67891233-abc'}): 26 | self.app.preprocess_request() 27 | self.assertEqual('1-67891234-def', current_request_id()) 28 | 29 | def test_default_request_id_parser_with_request_id(self): 30 | RequestID(self.app) 31 | with self.app.test_request_context(headers={'X-Request-Id': '1-67891234-def'}): 32 | self.app.preprocess_request() 33 | self.assertEqual('1-67891234-def', current_request_id()) 34 | 35 | def test_custom_request_id_parser(self): 36 | RequestID(self.app, request_id_parser=lambda: 'fixedid') 37 | with self.app.test_request_context(): 38 | self.app.preprocess_request() 39 | self.assertEqual('fixedid', current_request_id()) 40 | 41 | @patch('flask_log_request_id.request_id.uuid.uuid4') 42 | def test_custom_request_id_generator(self, mock_uuid4): 43 | mock_uuid4.return_value = 'abc-123' 44 | RequestID(self.app) 45 | with self.app.test_request_context(): 46 | self.app.preprocess_request() 47 | self.assertEqual('abc-123', current_request_id()) 48 | 49 | def test_disable_request_generator(self): 50 | 51 | self.app.config.update({ 52 | 'LOG_REQUEST_ID_GENERATE_IF_NOT_FOUND': False 53 | }) 54 | RequestID(self.app, request_id_generator=lambda: 'def-456') 55 | with self.app.test_request_context(): 56 | self.app.preprocess_request() 57 | self.assertIsNone(current_request_id()) 58 | 59 | def test_custom_generator(self): 60 | RequestID(self.app, request_id_generator=lambda: 'def-456') 61 | with self.app.test_request_context(): 62 | self.app.preprocess_request() 63 | self.assertEqual('def-456', current_request_id()) 64 | 65 | @patch('flask_log_request_id.request_id.logger') 66 | def test_log_request_when_enabled(self, mock_logger): 67 | self.app.config.update({ 68 | 'LOG_REQUEST_ID_LOG_ALL_REQUESTS': True 69 | }) 70 | RequestID(self.app) 71 | 72 | client = self.app.test_client() 73 | rv = client.get('/') 74 | self.assertTrue(b'hello world' in rv.data) 75 | 76 | mock_logger.info.assert_called_once_with('127.0.0.1 - - "GET / 200"') 77 | 78 | @patch('flask_log_request_id.request_id.logger') 79 | def test_log_request_disabled(self, mock_logger): 80 | RequestID(self.app) 81 | with self.app.test_request_context('/test'): 82 | pass 83 | 84 | mock_logger.info.assert_not_called() 85 | --------------------------------------------------------------------------------