1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
|
.. _integrations:
************
Integrations
************
Integrations are optional add-ins used to extend the functionality of the Django GUID middleware.
To enable an integration, simply add an integration instance to the ``INTEGRATIONS`` field in ``settings.py``,
and the relevant integration logic will be executed in the middleware:
.. code-block:: python
from django_guid.integrations import SentryIntegration
DJANGO_GUID = {
...
'INTEGRATIONS': [SentryIntegration()],
}
Integrations are a new addition to Django GUID, and we plan to expand selection in the future. If you are looking for specific functionality that is not yet available, consider creating an issue, making a pull request, or writing your own private integration. Custom integrations classes are simple to write and can be implemented just like package integrations.
Available integrations
======================
Sentry
------
Integrating with Sentry, lets you tag Sentry-issues with a ``transaction_id``. This lets you easily connect an event in Sentry to your logs.
.. image:: img/sentry.png
:width: 1600
:alt: Alternative text
Rather than changing how Sentry works, this is just an additional piece of metadata that you can use to link sources of information
about an exception. If you know the GUID of an exception, you can find the relevant Sentry issue by searching for the tag:
.. image:: img/sentry_search.png
:width: 1600
:alt: Alternative text
To add the integration, simply import ``SentryIntegration`` from the integrations folder and add it to your settings:
.. code-block:: python
from django_guid.integrations import SentryIntegration
DJANGO_GUID = {
...
'INTEGRATIONS': [SentryIntegration()],
}
Celery
------
The Celery integration enables tracing for Celery workers. There's three possible scenarios:
1. A task is published from a request within Django
2. A task is published from another task
3. A task is published from Celery Beat
For scenario 1 and 2 the existing correlation IDs is transferred, and for scenario
3 a unique ID is generated.
To enable this behavior, simply add it to your list of integrations:
.. code-block:: python
from django_guid.integrations import CeleryIntegration
DJANGO_GUID = {
...
'INTEGRATIONS': [
CeleryIntegration(
use_django_logging=True,
log_parent=True,
)
],
}
Integration settings
^^^^^^^^^^^^^^^^^^^^
These are the settings you can pass when instantiating the ``CeleryIntegration``:
* **use_django_logging**: Tells celery to use the Django logging configuration (formatter).
* **log_parent**: Enables the ``CeleryTracing`` log filter described below.
* **uuid_length**: Lets you optionally trim the length of the integration generated UUIDs.
* **sentry_integration**: If you use Sentry, enabling this setting will make sure ``transaction_id`` is set (like in the SentryIntegration) for Celery workers.
Celery integration log filter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Out of the box, the CeleryIntegration will make sure a correlation ID is present for any Celery task;
but how do you make sense of duplicate logs in subprocesses? Given these example tasks, what happens if we a worker
picks up ``debug_task`` as scheduled by Celery beat?
.. code-block:: python
@app.task()
def debug_task() -> None:
logger.info('Debug task 1')
second_debug_task.delay()
second_debug_task.delay()
@app.task()
def second_debug_task() -> None:
logger.info('Debug task 2')
third_debug_task.delay()
fourth_debug_task.delay()
@app.task()
def third_debug_task() -> None:
logger.info('Debug task 3')
fourth_debug_task.delay()
fourth_debug_task.delay()
@app.task()
def fourth_debug_task() -> None:
logger.info('Debug task 4')
It will be close to impossible to make sense of the logs generated,
simply because the correlation ID tells you nothing about how subprocesses are linked. For this,
the integration provides an additional log filter, ``CeleryTracing`` which logs the
ID of the current process and the ID of the parent process. Using the log filter, the log output of the example tasks becomes:
.. code-block:: bbcode
correlation-id current-id
| parent-id |
| | |
INFO [3b162382e1] [ None ] [93ddf3639c] demoproj.celery - Debug task 1
INFO [3b162382e1] [93ddf3639c] [24046ab022] demoproj.celery - Debug task 2
INFO [3b162382e1] [93ddf3639c] [cb5595a417] demoproj.celery - Debug task 2
INFO [3b162382e1] [24046ab022] [08f5428a66] demoproj.celery - Debug task 3
INFO [3b162382e1] [24046ab022] [32f40041c6] demoproj.celery - Debug task 4
INFO [3b162382e1] [cb5595a417] [1c75a4ed2c] demoproj.celery - Debug task 3
INFO [3b162382e1] [08f5428a66] [578ad2d141] demoproj.celery - Debug task 4
INFO [3b162382e1] [cb5595a417] [21b2ef77ae] demoproj.celery - Debug task 4
INFO [3b162382e1] [08f5428a66] [8cad7fc4d7] demoproj.celery - Debug task 4
INFO [3b162382e1] [1c75a4ed2c] [72a43319f0] demoproj.celery - Debug task 4
INFO [3b162382e1] [1c75a4ed2c] [ec3cf4113e] demoproj.celery - Debug task 4
At the very least, this should provide a mechanism for linking parent/children processes
in a meaningful way.
To set up the filter, add :code:`django_guid.integrations.celery.log_filters.CeleryTracing` as a filter in your ``LOGGING`` configuration:
.. code-block:: python
LOGGING = {
...
'filters': {
'celery_tracing': {
'()': 'django_guid.integrations.celery.log_filters.CeleryTracing'
}
}
}
Put that filter in your handler:
.. code-block:: python
LOGGING = {
...
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'medium',
'filters': ['correlation_id', 'celery_tracing'],
}
}
}
And then you can **optionally** add ``celery_parent_id`` and/or ``celery_current_id`` to you formatter:
.. code-block:: python
LOGGING = {
...
'formatters': {
'medium': {
'format': '%(levelname)s [%(correlation_id)s] [%(celery_parent_id)s-%(celery_current_id)s] %(name)s - %(message)s'
}
}
}
However, if you use a log management tool which lets you interact with ``log.extra`` value, leaving the filters
out of the formatter might be preferable.
If these settings were confusing, please have a look in the demo projects'
`settings.py <https://github.com/snok/django-guid/blob/master/demoproj/settings.py>`_ file for a complete example.
Writing your own integration
============================
Creating your own custom integration requires you to inherit the ``Integration`` base class (which is found `here <https://github.com/snok/django-guid/tree/master/django_guid/integrations/base>`_).
The class is quite simple and only contains four methods and a class attribute:
.. code-block:: python
class Integration(object):
"""
Integration base class.
"""
identifier = None # The name of your integration
def __init__(self) -> None:
if self.identifier is None:
raise ImproperlyConfigured('`identifier` cannot be None')
def setup(self) -> None:
"""
Holds validation and setup logic to be run when Django starts.
"""
pass
def run(self, guid: str, **kwargs) -> None:
"""
Code here is executed in the middleware, before the view is called.
"""
raise ImproperlyConfigured(f'The integration `{self.identifier}` is missing a `run` method')
def cleanup(self, **kwargs) -> None:
"""
Code here is executed in the middleware, after the view is called.
"""
pass
To extend this into a fully functioning integration, all you need to do is
1. Create a new class that inherits the base class
2. Set the identifier to a string, naming your integration
3. Add the logic you wish to be executed to the ``run`` method
4. Add logic to each of the remaining methods as required
A fully functioning integration can be as simple as this:
.. code-block:: python
from django_guid.integrations import Integration
class CustomIntegration(Integration):
identifier = 'CustomIntegration' # Should be a string
def run(self, guid, **kwargs):
print('This is a functioning Django GUID integration')
There are four built in methods which are always called. You can chose to override these in your custom
integration.
Method descriptions
--------------------
Setup
^^^^^
The ``setup`` method is run when Django starts, and is a good place to keep your integration-specific validation logic,
like, e.g., making sure all dependencies are installed:
.. code-block:: python
from third_party_sdk import start_service
class CustomIntegration(Integration):
identifier = 'CustomIntegration'
def setup(self):
try:
import third_party_sdk
except ModuleNotFoundError:
raise ImproperlyConfigured(
'Package third_party_sdk must be installed'
)
Run
^^^
The ``run`` method is required, and is designed to hold code that should be executed each time the middleware is run
(for each request made to the server), before the view is called.
This function **must** accept both ``guid`` and ``**kwargs``. Additional arguments are likely be added
in the future, and so the function must be able to handle those new arguments.
.. code-block:: python
from third_party_sdk import send_guid_to_system
class CustomIntegration(Integration):
identifier = 'CustomIntegration'
def setup(self):
...
def run(self, guid, **kwargs):
send_guid_to_system(guid=guid)
Cleanup
^^^^^^^
The ``cleanup`` method is the final method called in the middleware, each time the middleware, each time the middleware is run,
after a view has been called.
This function **must** accept ``**kwargs``. Additional arguments are likely be added
in the future, and so the function must be able to handle those new arguments.
.. code-block:: python
from third_party_sdk import clean_up_guid
class CustomIntegration(Integration):
identifier = 'CustomIntegration'
def setup(self):
...
def run(self, guid, **kwargs):
...
def cleanup(self, **kwargs):
clean_up_guid()
|