# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['asgi_correlation_id', 'asgi_correlation_id.extensions']

package_data = \
{'': ['*']}

setup_kwargs = {
    'name': 'asgi-correlation-id',
    'version': '0.1.6',
    'description': 'Middleware for setting and propagating correlation/request IDs',
    'long_description': "[![pypi](https://img.shields.io/pypi/v/asgi-correlation-id)](https://pypi.org/project/asgi-correlation-id/)\n[![test](https://github.com/snok/asgi-correlation-id/actions/workflows/test.yml/badge.svg)](https://github.com/snok/asgi-correlation-id/actions/workflows/test.yml)\n[![codecov](https://codecov.io/gh/snok/asgi-correlation-id/branch/main/graph/badge.svg?token=1aXlWPm2gb)](https://codecov.io/gh/snok/asgi-correlation-id)\n\n# ASGI Correlation ID middleware\n\nMiddleware for loading or generating correlation IDs, making it simple to correlate logs from a single HTTP request.\n\nCorrelation IDs are loaded when the middleware detects a correlation ID HTTP header in an incoming request. When no\nheader is found, a correlation ID is generated for the request instead.\n\nThe header key name is customizable. Platforms like Heroku use `X-Request-ID` as their\nstandard ([source](https://devcenter.heroku.com/articles/http-request-id)), but `X-Correlation-ID` is also commonly\nused.\n\nOnce a correlation ID has been set or generated, it's added to your project logs with a log filter.\n\n# Table of contents\n\n- [Installation](#installation)\n- [Configuring the middleware](#configuring-the-middleware)\n    - [1/3 Setting up the middleware](#setting-up-the-middleware)\n    - [2/3 Settings](#settings)\n    - [3/3 Configuring logging](#configuring-logging)\n- [Extensions](#extensions)\n    - [Sentry](#sentry)\n    - [Celery](#celery)\n        - [Taking it one step further - Adding Celery tracing IDs](#taking-it-one-step-further---adding-celery-tracing-ids)\n\n# Installation\n\n```\npip install asgi-correlation-id\n```\n\n# Configuring the middleware\n\nTo set up the package, you need to add the middleware and configure logging.\n\n## 1/3 Setting up the middleware\n\nThe middleware can be added like this\n\n```python\nfrom fastapi import FastAPI\nfrom starlette.middleware import Middleware\n\nfrom asgi_correlation_id import CorrelationIdMiddleware\n\n\napp = FastAPI(middleware=[Middleware(CorrelationIdMiddleware)])\n```\n\nor like this\n\n```python\nfrom fastapi import FastAPI\n\nfrom asgi_correlation_id import CorrelationIdMiddleware\n\n\napp = FastAPI()\napp.add_middleware(CorrelationIdMiddleware)\n```\n\nor any other way your framework allows.\n\nFor [Starlette](https://github.com/encode/starlette) apps, just substitute `FastAPI` with `Starlette` in the example\nabove.\n\n## 2/3 Settings\n\nThe middleware has a few settings. These are the defaults:\n\n```python\nclass CorrelationIdMiddleware(\n    header_name='X-Request-ID',\n    validate_header_as_uuid=True,\n): ...\n```\n\n### header_name\n\nThe HTTP header key to read IDs from.\n\nIn addition to the default, another popular choice for header name is `X-Correlation-ID`, but the header name can be any\nstring.\n\nDefaults to `X-Request-ID`.\n\n### validate_header_as_uuid\n\nBy default, the middleware validates correlation IDs as valid UUIDs. If turned off, any string will be accepted.\nThe benefit of enforcing UUID usage has to do with limiting the chance of correlation ID collisions.\n\n\nWhen a header value is detected but found to be invalid, the value is discarded, and a fresh UUID is generated in its\nplace. The package logs a warning in these cases.\n\n\nDefaults to `True`.\n\n## 3/3 Configuring logging\n\nTo set up logging of the correlation ID, you simply have to implement the log-filter supplied by the package.\n\nIf your logging config looks something like this:\n\n```python\nLOGGING = {\n    'version': 1,\n    'disable_existing_loggers': False,\n    'formatters': {\n        'web': {\n            'class': 'logging.Formatter',\n            'datefmt': '%H:%M:%S',\n            'format': '%(levelname)s ... %(name)s %(message)s',\n        },\n    },\n    'handlers': {\n        'web': {\n            'class': 'logging.StreamHandler',\n            'formatter': 'web',\n        },\n    },\n    'loggers': {\n        'my_project': {\n            'handlers': ['web'],\n            'level': 'DEBUG',\n            'propagate': True,\n        },\n    },\n}\n```\n\nYou simply have to add a log filter, like this\n\n```diff\n+ from asgi_correlation_id.log_filters import correlation_id_filter\n\nLOGGING = {\n    'version': 1,\n    'disable_existing_loggers': False,\n+   'filters': {\n+       'correlation_id': {'()': correlation_id_filter(uuid_length=32)},\n+   },\n    'formatters': {\n        'web': {\n            'class': 'logging.Formatter',\n            'datefmt': '%H:%M:%S',\n+           'format': '%(levelname)s ... [%(correlation_id)s] %(name)s %(message)s',\n        },\n    },\n    'handlers': {\n        'web': {\n            'class': 'logging.StreamHandler',\n+           'filters': ['correlation_id'],\n            'formatter': 'web',\n        },\n    },\n    'loggers': {\n        'my_project': {\n            'handlers': ['web'],\n            'level': 'DEBUG',\n            'propagate': True,\n        },\n    },\n}\n```\n\nAnd your log output will go from this\n\n```\nINFO ... project.views This is a DRF view log, and should have a GUID.\nWARNING ... project.services.file Some warning in a function\nINFO ... project.views This is a DRF view log, and should have a GUID.\nINFO ... project.views This is a DRF view log, and should have a GUID.\nWARNING ... project.services.file Some warning in a function\nWARNING ... project.services.file Some warning in a function\n```\n\nto this\n\n```docker\nINFO ... [773fa6885e03493498077a273d1b7f2d] project.views This is a DRF view log, and should have a GUID.\nWARNING ... [773fa6885e03493498077a273d1b7f2d] project.services.file Some warning in a function\nINFO ... [0d1c3919e46e4cd2b2f4ac9a187a8ea1] project.views This is a DRF view log, and should have a GUID.\nINFO ... [99d44111e9174c5a9494275aa7f28858] project.views This is a DRF view log, and should have a GUID.\nWARNING ... [0d1c3919e46e4cd2b2f4ac9a187a8ea1] project.services.file Some warning in a function\nWARNING ... [99d44111e9174c5a9494275aa7f28858] project.services.file Some warning in a function\n```\n\nIf you're using a json log-formatter, just add `correlation-id: %(correlation_id)s` to your list of properties.\n\n# Extensions\n\nWe've added a couple of (we think) nice extensions to extend the scope of correlation IDs.\n\n## Sentry\n\nIf your project has [sentry-sdk](https://pypi.org/project/sentry-sdk/)\ninstalled, correlation IDs will automatically be added to Sentry events as a `transaction_id`.\n\nSee\nthis [blogpost](https://blog.sentry.io/2019/04/04/trace-errors-through-stack-using-unique-identifiers-in-sentry#1-generate-a-unique-identifier-and-set-as-a-sentry-tag-on-issuing-service)\nfor a little bit of detail.\n\n## Celery\n\nThere's one issue with Celery: when calling `task.delay()` from the web server, the worker process does not have access\nto the `correlation_id` context var, so it will not include the request correlation id by default.\n\nTo make sure correlation IDs are transferred to worker processes we use Celery signal hooks that transfer and receive\ncorrelation IDs, like this:\n\n```python\n@before_task_publish.connect()\ndef transfer_correlation_id(headers) -> None:\n    headers['REQUEST_ID'] = correlation_id.get()\n\n\n@task_prerun.connect()\ndef load_correlation_id(task) -> None:\n    id_value = task.request.get('REQUEST_ID')\n    correlation_id.set(id_value)\n```\n\nTo make the setup as simple as possible, we've put all the required code into a single setup function, so to enable\ncorrelation IDs for Celery, just add this code to your `celery.py`:\n\n```python\nfrom asgi_correlation_id.extensions.celery import load_correlation_ids\n\nload_correlation_ids()\n```\n\n### Taking it one step further - Adding Celery tracing IDs\n\nIn addition to transferring request IDs to Celery workers, we've added one more log filter for improving tracing in\ncelery processes. This is completely separate from the middleware, but is something we use personally, so maintain here\nregardless.\n\nThe log filter adds an ID, `celery_current_id` for each worker process, and an ID, `celery_parent_id` for the process\nthat spawned it.\n\nHere's a quick summary of outputs from different scenarios:\n\n| Scenario                                           | Correlation ID     | Celery Current ID | Celery Parent ID |\n|------------------------------------------          |--------------------|-------------------|------------------|\n| Request                                            | ✅                |                   |                  |\n| Request -> Worker                                  | ✅                | ✅               |                  |\n| Request -> Worker -> Another worker                | ✅                | ✅               | ✅              |\n| Beat -> Worker     | ✅*               | ✅                |                   |                  |\n| Beat -> Worker -> Worker     | ✅*     | ✅                | ✅               | ✅              |\n\n*When we're in a process spawned separately from an HTTP request, a correlation ID is still spawned for the first\nprocess in the chain, and passed down. You can think of the correlation ID as an origin ID, while the combination of\ncurrent and parent-ids as a way of linking the chain.\n\nTo add the current and parent IDs, just alter your `celery.py` to this:\n\n```diff\n+ from asgi_correlation_id.extensions.celery import load_correlation_ids, load_celery_current_and_parent_ids\n\nload_correlation_ids()\n+ load_celery_current_and_parent_ids()\n```\n\nTo set up the additional log filters, update your log config like this:\n\n```diff\n+ from asgi_correlation_id.log_filters import celery_tracing_id_filter, correlation_id_filter\n\nLOGGING = {\n    'version': 1,\n    'disable_existing_loggers': False,\n    'filters': {\n        'correlation_id': {'()': correlation_id_filter(uuid_length=32)},\n+       'celery_tracing': {'()': celery_tracing_id_filter(uuid_length=32)},\n    },\n    'formatters': {\n        'web': {\n            'class': 'logging.Formatter',\n            'datefmt': '%H:%M:%S',\n            'format': '%(levelname)s ... [%(correlation_id)s] %(name)s %(message)s',\n        },\n+       'celery': {\n+           'class': 'logging.Formatter',\n+           'datefmt': '%H:%M:%S',\n+           'format': '%(levelname)s ... [%(correlation_id)s] [%(celery_parent_id)s-%(celery_current_id)s] %(name)s %(message)s',\n+       },\n    },\n    'handlers': {\n        'web': {\n            'class': 'logging.StreamHandler',\n            'filters': ['correlation_id'],\n            'formatter': 'web',\n        },\n+       'celery': {\n+           'class': 'logging.StreamHandler',\n+           'filters': ['correlation_id', 'celery_tracing'],\n+           'formatter': 'celery',\n+       },\n    },\n    'loggers': {\n        'my_project': {\n+           'handlers': ['celery' if any('celery' in i for i in sys.argv) else 'web'],\n            'level': 'DEBUG',\n            'propagate': True,\n        },\n    },\n}\n```\n",
    'author': 'Sondre Lillebø Gundersen',
    'author_email': 'sondrelg@live.no',
    'maintainer': 'Jonas Krüger Svensson',
    'maintainer_email': 'jonas-ks@hotmail.com',
    'url': 'https://github.com/snok/asgi-correlation-id',
    'packages': packages,
    'package_data': package_data,
    'python_requires': '>=3.7,<4.0',
}


setup(**setup_kwargs)
