# -*- 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.4',
    'description': 'Middleware that propagates HTTP header correlation IDs to project logs',
    '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 generating or propagating correlation IDs, making it possible to connect each of your\nlogs to a single HTTP request.\n\nCorrelation IDs are propagated when HTTP requests contain the `Correlation-ID` HTTP header key,\nand generated when no header is present. The key value of the HTTP header can be customized,\nand if you\'re, e.g., on a platform like [Heroku](https://devcenter.heroku.com/articles/http-request-id),\nyou should use `X-Request-ID` instead.\n\nIn addition to the core functionality, the package supports forwarding correlation IDs\nto [Sentry](https://sentry.io/organizations/otovo/issues/) events\nand [Celery](https://docs.celeryproject.org/en/stable/getting-started/introduction.html)\ntasks. See the extensions section below for details.\n\n# Table of contents\n\n- [Installation](#installation)\n- [Setting up the middleware](#setting-up-the-middleware)\n    - [Adding the middleware](#adding-the-middleware)\n    - [Middleware settings](#middleware-settings)\n    - [Configuring logging](#configuring-logging)\n- [Extensions](#extensions)\n  - [Sentry](#sentry)\n  - [Celery](#celery)\n- [Extending Celery further](#setting-up-celery-support)\n    - [The feature](#the-feature)\n    - [Adding Celery event hooks](#adding-celery-event-hooks)\n    - [Celery event hook settings](#celery-event-hook-settings)\n    - [Configuring Celery logging](#configuring-celery-logging)\n\n\n# Installation\n\n```python\npip install asgi-correlation-id\n```\n\n# Setting up the middleware\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\napp = FastAPI(middleware=[Middleware(CorrelationIdMiddleware)])\n```\n\nor this\n\n```python\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 above.\n\n## Middleware settings\n\nThe middleware has a few settings. These are the defaults:\n\n```python\nclass CorrelationIdMiddleware(\n    header_name=\'Correlation-ID\',\n    validate_guid=True,\n    uuid_length=32,\n)\n```\n\nEach individual setting is described below:\n\n### header_name\n\nThe HTTP header key to read IDs from.\n\nIn additon to `Correlation-ID`, another popular choice for header name is `X-Request-ID`. Among other things, this\nis the standard header value for request IDs on [Heroku](https://devcenter.heroku.com/articles/http-request-id).\n\nDefaults to `Correlation-ID`.\n\n### validate_guid\n\nBy default, the middleware validates correlation IDs\nas valid UUIDs. If turned off, any string will be accepted.\n\nAn invalid header is discarded, and a fresh UUID is generated in its place.\n\nDefaults to `True`.\n\n### uuid_length\n\nLets you optionally trim the length of correlation IDs.\nProbably not needed in most cases, but for, e.g., local development\nhaving 32-length UUIDs in every log output to\nyour console *can* be excessive.\n\nDefaults to `32`.\n\n## Configuring logging\n\nTo get a benefit from the middleware, you\'ll want to configure your logging setup to log the correlation ID in some form\nor another. This way logs can be correlated to a single request - which is largely the point of the middleware.\n\nTo set up logging of the correlation ID, you simply have to implement the 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\nLOGGING = {\n    \'version\': 1,\n    \'disable_existing_loggers\': False,\n+   \'filters\': {\n+       \'correlation_id\': {\'()\': CorrelationId},\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 containing a correlation ID, connecting each log to a single request\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 things like a json-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\na `transaction_id`.\n\nSee this [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\nCalling `task.delay()` in the context of a HTTP request would normally mean that you lose\nthe correlation ID completely, because your work is picked up by a separate worker in a separate context.\n\nTo make sure correlation IDs persist across this jump, we load Celery signal hooks that transfer and receive\ncorrelation IDs from and to task headers.\n\nThis behavior is enabled as long as [celery](https://pypi.org/project/celery/) is installed.\n\n# Extending Celery further\n\nLoading correlation IDs from a HTTP request to a background task is enabled by default.\n\nIn addition to this though, the package provides one more set of hooks you can use to improve\ntracing in Celery.\n\n## The feature\n\nIn the case of a HTTP request spawning a background task, we have full information about the sequence of events.\nBut what happens if that background task spawns more background tasks, or retries and rejections are added to the mix?\nAs soon as more than one task is spawned, the correlation ID is reduced to an "origin ID" -> the ID of the HTTP request\nthat spawned the first worker.\n\nIn the same way correlation IDs are nice, because it connects logs to a single HTTP request, we would like something to\ngive us the sequence of events when things get complicated. For this purpose we can extend the concept of a correlation\nid by adding a few more IDs. We therefore provide two more log filters:\n\n- A `current_id`, which is a generated UUID, unique to each new worker process\n- A `parent_id` which is the `current_id` of the process that issued the current task.\n\nSo to summarize, if you add all Celery hooks, you would end up with:\n\n- A `correlation_id`: The ID of an originating HTTP request or a generated ID in the case of scheduled tasks\n- A `current_id`: The ID of the current worker process\n- A `parent_id`: The ID of the parent worker process if one existed.\n\nThis means all logs can be correlated to a single correlation ID, and the sequence of events\ncan be completely reconstructed, since each new task will have a reference to which process issued it.\n\n## Adding Celery event hooks\n\nSetting up the event hooks is simple, just import `configure_celery_current_and_parent_id` and run it during startup.\n\n```python\nfrom fastapi import FastAPI\n\nfrom asgi_correlation_id import configure_celery_current_and_parent_id\n\napp = FastAPI()\n\napp.add_event_handler(\'startup\', configure_celery_current_and_parent_id)\n```\n\nYou can look over the event\nhooks [here](https://github.com/snok/asgi-correlation-id/blob/main/asgi_correlation_id/extensions/celery.py).\n\n## Celery event hook settings\n\nThe setup function has a single setting.\n\n### uuid_length\n\nLets you optionally trim the length of IDs.\nProbably not needed in most cases, but for, e.g., local development\nhaving 32-length UUIDs in every log output to\nyour console *can* be excessive.\n\nDefaults to `32`.\n\n## Configuring logging\n\nIf this is your logging config after setting up the middleware\n\n```python\nLOGGING = {\n    \'version\': 1,\n    \'disable_existing_loggers\': False,\n    \'filters\': {\n        \'correlation_id\': {\'()\': CorrelationId},\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\nYou simply need to add these lines of code to start logging the `current_id` and `parent_id`\n\n```diff\nLOGGING = {\n    \'version\': 1,\n    \'disable_existing_loggers\': False,\n    \'filters\': {\n        \'correlation_id\': {\'()\': CorrelationId},\n+       \'celery_tracing\': {\'()\': CeleryTracingIds},\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\nThis example implements separate logging handlers and formatters for Celery and non-celery processes, but that\'s\nnot strictly necessary. Using a JSON-formatter is probably also desired once you\nget past a small number of log filters, since logs quickly become pretty cluttered.\n\nDuring development though, using the UUID length can be useful for limiting noise. Something like this\nis what we would use ourselves:\n\n```python\n{\n    \'version\': 1,\n    \'disable_existing_loggers\': False,\n    \'filters\': {\n        \'correlation_id\': {\'()\': CorrelationId},\n        \'celery_tracing\': {\'()\': CeleryTracingIds},\n    },\n    \'formatters\': {\n        \'dev\': {\n            \'class\': \'logging.Formatter\',\n            \'datefmt\': \'%H:%M:%S\',\n            \'format\': \'%(levelname)s:\\t\\b%(asctime)s %(name)s:%(lineno)d [%(correlation_id)s] %(message)s\',\n        },\n        \'dev-celery\': {\n            \'class\': \'logging.Formatter\',\n            \'datefmt\': \'%H:%M:%S\',\n            \'format\': (\n                \'%(levelname)s:\\t\\b%(asctime)s %(name)s:%(lineno)d [%(correlation_id)s]\'\n                \' [%(celery_parent_id)s-%(celery_current_id)s] %(message)s\'\n            ),\n        },\n        \'json\': {\n            \'()\': \'pythonjsonlogger.jsonlogger.JsonFormatter\',\n            \'format\': """\n                asctime: %(asctime)s\n                created: %(created)f\n                filename: %(filename)s\n                funcName: %(funcName)s\n                levelname: %(levelname)s\n                level: %(levelname)s\n                levelno: %(levelno)s\n                lineno: %(lineno)d\n                message: %(message)s\n                module: %(module)s\n                msec: %(msecs)d\n                name: %(name)s\n                pathname: %(pathname)s\n                process: %(process)d\n                processName: %(processName)s\n                relativeCreated: %(relativeCreated)d\n                thread: %(thread)d\n                threadName: %(threadName)s\n                exc_info: %(exc_info)s\n                correlation-id: %(correlation_id)s\n                celery-current-id: %(celery_current_id)s\n                celery-parent-id: %(celery_parent_id)s\n            """,\n            \'datefmt\': \'%Y-%m-%d %H:%M:%S\',\n        },\n    },\n    \'handlers\': {\n        \'dev\': {\n            \'class\': \'logging.StreamHandler\',\n            \'stream\': \'ext://sys.stdout\',\n            \'filters\': [\'correlation_id\'],\n            \'formatter\': \'console\',\n        },\n        \'dev-celery\': {\n            \'class\': \'logging.StreamHandler\',\n            \'stream\': \'ext://sys.stdout\',\n            \'filters\': [\'correlation_id\', \'celery_tracing\'],\n            \'formatter\': \'console-celery\',\n        },\n        \'json\': {\n            \'class\': \'logging.StreamHandler\',\n            \'stream\': \'ext://sys.stdout\',\n            \'filters\': [\'correlation_id\'],\n            \'formatter\': \'json\',\n        },\n    },\n    \'loggers\': {\n        \'my_project\': {\n            \'handlers\': [\n                \'json\' if settings.ENVIRONMENT != \'dev\'\n                else \'dev-celery\' if any(\'celery\' in i for i in sys.argv)\n                else \'dev\'\n            ],\n            \'level\': \'DEBUG\',\n            \'propagate\': True,\n        },\n    },\n}\n```\n',
    'author': 'Jonas Krüger Svensson',
    'author_email': 'jonas-ks@hotmail.com',
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://github.com/snok/asgi-correlation-id',
    'packages': packages,
    'package_data': package_data,
    'python_requires': '>=3.7,<4.0',
}


setup(**setup_kwargs)
