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

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

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

extras_require = \
{':python_version == "3.6"': ['dataclasses>=0.8,<0.9', 'contextvars>=2.4,<3.0']}

setup_kwargs = {
    'name': 'asgi-correlation-id',
    'version': '1.1.0',
    'description': 'Middleware correlating project logs to individual requests',
    '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 for each incoming request. Correlation IDs can be added to your\nlogs, making it simple to retrieve all logs generated from a single HTTP request.\n\nWhen the middleware detects a correlation ID HTTP header in an incoming request, the ID is stored. If no header is\nfound, a correlation ID is generated for the request instead.\n\nThe middleware checks for the `X-Request-ID` header by default, but can be set to any key.\n`X-Correlation-ID` is also pretty commonly used.\n\n## Example\n\nOnce logging is configure, your output will go from this\n\n```\nINFO    ... project.views  This is an info log\nWARNING ... project.models This is a warning log\nINFO    ... project.views  This is an info log\nINFO    ... project.views  This is an info log\nWARNING ... project.models This is a warning log\nWARNING ... project.models This is a warning log\n```\n\nto this\n\n```docker\nINFO    ... [773fa6885] project.views  This is an info log\nWARNING ... [773fa6885] project.models This is a warning log\nINFO    ... [0d1c3919e] project.views  This is an info log\nINFO    ... [99d44111e] project.views  This is an info log\nWARNING ... [0d1c3919e] project.models This is a warning log\nWARNING ... [99d44111e] project.models This is a warning log\n```\n\nNow we're actually able to see which logs are related.\n\n# Installation\n\n```\npip install asgi-correlation-id\n```\n\n# Setup\n\nTo set up the package, you need to add the middleware and configure logging.\n\n## Adding 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\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\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\nThe middleware only has two settings:\n\n```python\nclass CorrelationIdMiddleware(\n    # The HTTP header key to read IDs from.\n    header_name='X-Request-ID',\n    # Enforce UUID formatting to limit chance of collisions\n    # - Invalid header values are discarded, and an ID is generated in its place\n    validate_header_as_uuid=True,\n): ...\n```\n\n## Configure logging\n\nTo set up logging of the correlation ID, you simply have to add the log-filter the package provides.\n\nIf your current log-config looked 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 the 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\nIf you're using a json log-formatter, just add `correlation-id: %(correlation_id)s` to your list of properties.\n\n# Extensions\n\nIn addition to the middleware, we've added a couple of extensions for third-party packages.\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. The transaction ID is displayed in the event detail view in Sentry and is just an easy way\nto connect logs to a Sentry event.\n\n## Celery\n\nFor Celery user's there's one primary issue: workers run as completely separate processes, so correlation IDs\nare lost when spawning background tasks from requests.\n\nHowever, with some Celery signal magic, we can actually transfer correlation IDs to worker processes, like this:\n\n```python\n@before_task_publish.connect()\ndef transfer_correlation_id(headers) -> None:\n    # This is called before task.delay() finishes\n    # Here we're able to transfer the correlation ID via the headers kept in our backend\n    headers[header_key] = correlation_id.get()\n\n\n@task_prerun.connect()\ndef load_correlation_id(task) -> None:\n    # This is called when the worker picks up the task\n    # Here we're able to load the correlation ID from the headers\n    id_value = task.request.get(header_key)\n    correlation_id.set(id_value)\n```\n\nTo configure correlation ID transfer, simply import and run the setup function the package provides:\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 correlation ID functionality, but is something we use ourselves,\nso keep in the package with the rest of the signals.\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\nWith these IDs configured you should be able to:\n\n1. correlate all logs from a single origin, and\n2. piece together the order each log was run, and which process spawned which\n\n#### Example\n\nWith everything configured, assuming you have a set of tasks like this\n\n```python\n@celery.task()\ndef debug_task():\n    logger.info('Debug task 1')\n    second_debug_task.delay()\n    second_debug_task.delay()\n\n\n@celery.task()\ndef second_debug_task():\n    logger.info('Debug task 2')\n    third_debug_task.delay()\n    fourth_debug_task.delay()\n\n\n@celery.task()\ndef third_debug_task():\n    logger.info('Debug task 3')\n    fourth_debug_task.delay()\n    fourth_debug_task.delay()\n\n\n@celery.task()\ndef fourth_debug_task():\n    logger.info('Debug task 4')\n```\n\nyour logs could look something like this\n\n```\n   correlation-id               current-id\n          |        parent-id        |\n          |            |            |\nINFO [3b162382e1] [   None   ] [93ddf3639c] project.tasks - Debug task 1\nINFO [3b162382e1] [93ddf3639c] [24046ab022] project.tasks - Debug task 2\nINFO [3b162382e1] [93ddf3639c] [cb5595a417] project.tasks - Debug task 2\nINFO [3b162382e1] [24046ab022] [08f5428a66] project.tasks - Debug task 3\nINFO [3b162382e1] [24046ab022] [32f40041c6] project.tasks - Debug task 4\nINFO [3b162382e1] [cb5595a417] [1c75a4ed2c] project.tasks - Debug task 3\nINFO [3b162382e1] [08f5428a66] [578ad2d141] project.tasks - Debug task 4\nINFO [3b162382e1] [cb5595a417] [21b2ef77ae] project.tasks - Debug task 4\nINFO [3b162382e1] [08f5428a66] [8cad7fc4d7] project.tasks - Debug task 4\nINFO [3b162382e1] [1c75a4ed2c] [72a43319f0] project.tasks - Debug task 4\nINFO [3b162382e1] [1c75a4ed2c] [ec3cf4113e] project.tasks - Debug task 4\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,
    'extras_require': extras_require,
    'python_requires': '>=3.6.1,<4.0.0',
}


setup(**setup_kwargs)
