aiohttp-request-id-logging 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ """
2
+ Adds a request (correlation) id to log messages in aiohttp web applications.
3
+
4
+ Usage:
5
+
6
+ from aiohttp.web import Application, Response, RouteTableDef, run_app
7
+ from logging import DEBUG, basicConfig
8
+ from aiohttp_request_id_logging import (
9
+ setup_logging_request_id_prefix,
10
+ RequestIdMiddleware,
11
+ RequestIdAccessLogger,
12
+ )
13
+
14
+ routes = RouteTableDef()
15
+
16
+ @routes.get('/')
17
+ async def hello(request):
18
+ return Response(text="Hello, world!")
19
+
20
+ basicConfig(
21
+ level=DEBUG,
22
+ format='%(asctime)s [%(threadName)s] %(name)-37s %(levelname)5s: %(requestIdPrefix)s%(message)s')
23
+
24
+ setup_logging_request_id_prefix()
25
+
26
+ app = Application(middlewares=[RequestIdMiddleware()])
27
+ app.router.add_routes(routes)
28
+
29
+ run_app(app, access_log_class=RequestIdAccessLogger)
30
+
31
+ """
32
+
33
+ __version__ = "1.0.0"
34
+
35
+ try:
36
+ import sentry_sdk
37
+ except ImportError:
38
+ sentry_sdk = None # ty: ignore[invalid-assignment]
39
+
40
+ from .context import request_id, REQUEST_ID_KEY, FALLBACK_REQUEST_ID_KEY
41
+ from .errors import RequestIdKeyAlreadySetError
42
+ from .middleware import RequestIdMiddleware, request_id_middleware, noop
43
+ from .request_id_factories import (
44
+ random_request_id_factory,
45
+ sequential_request_id_factory,
46
+ SequentialRequestIdFactory,
47
+ )
48
+ from .logging_setup import setup_logging_request_id_prefix, RequestIdAccessLogger
49
+
50
+
51
+ # old names for backward compatibility
52
+ generate_request_id = random_request_id_factory
53
+ RequestIdContextAccessLogger = RequestIdAccessLogger
54
+
55
+
56
+ __all__ = [
57
+ "RequestIdMiddleware",
58
+ "request_id_middleware",
59
+ "RequestIdKeyAlreadySetError",
60
+ "setup_logging_request_id_prefix",
61
+ "RequestIdAccessLogger",
62
+ "RequestIdContextAccessLogger",
63
+ "random_request_id_factory",
64
+ "generate_request_id",
65
+ "sequential_request_id_factory",
66
+ "SequentialRequestIdFactory",
67
+ "request_id",
68
+ "REQUEST_ID_KEY",
69
+ "FALLBACK_REQUEST_ID_KEY",
70
+ "noop",
71
+ ]
@@ -0,0 +1,24 @@
1
+ """
2
+ Constants and context variables shared across the package.
3
+ """
4
+
5
+ from contextvars import ContextVar
6
+ from aiohttp import web
7
+
8
+
9
+ # ContextVar that contains given request tracing id,
10
+ # or None outside of a request
11
+ request_id: ContextVar[str | None] = ContextVar("request_id", default=None)
12
+
13
+ REQUEST_ID_KEY: "web.RequestKey[str] | str"
14
+ FALLBACK_REQUEST_ID_KEY: str | None
15
+
16
+ try:
17
+ # key for storing the request id in the request; aiohttp recommends
18
+ # web.RequestKey instances instead of plain strings
19
+ REQUEST_ID_KEY = web.RequestKey("request_id", str)
20
+ FALLBACK_REQUEST_ID_KEY = "request_id"
21
+ except AttributeError:
22
+ # older aiohttp without web.RequestKey
23
+ REQUEST_ID_KEY = "request_id"
24
+ FALLBACK_REQUEST_ID_KEY = None
@@ -0,0 +1,15 @@
1
+ class RequestIdKeyAlreadySetError(Exception):
2
+ """
3
+ Raised when the request already contains a request id.
4
+
5
+ This most likely means that request_id_middleware is applied twice,
6
+ or that something else also sets the request id in the request.
7
+ """
8
+
9
+ def __init__(self, existing_request_id: str):
10
+ super().__init__(
11
+ f"The request already contains request id {existing_request_id!r} - "
12
+ "request_id_middleware is most likely applied twice, "
13
+ "or something else also sets the request id"
14
+ )
15
+ self.existing_request_id = existing_request_id
@@ -0,0 +1,66 @@
1
+ import logging
2
+
3
+ from aiohttp import web
4
+ from aiohttp.web_log import AccessLogger as _AccessLogger
5
+
6
+ from .context import request_id, REQUEST_ID_KEY
7
+
8
+
9
+ def setup_logging_request_id_prefix(prefix_format: str = "[req:{request_id}] ") -> None:
10
+ """
11
+ Wrap logging record factory so that every log record gets two extra attributes:
12
+
13
+ - record.requestIdPrefix - "[req:...] ", or an empty string outside of a request
14
+ - record.request_id - the raw request id, or None
15
+
16
+ You can then use them in log format as "%(requestIdPrefix)s" or "%(request_id)s".
17
+
18
+ The prefix can be customized with the prefix_format parameter.
19
+
20
+ Safe to call multiple times - the setup is done only once; subsequent
21
+ calls do nothing, even when called with a different prefix_format
22
+ (the format from the first call stays in effect).
23
+ """
24
+ # make sure we are doing this only once
25
+ if getattr(logging, "request_id_log_record_factory_set_up", False):
26
+ return
27
+ logging.request_id_log_record_factory_set_up = True # ty: ignore[unresolved-attribute]
28
+
29
+ old_factory = logging.getLogRecordFactory()
30
+
31
+ def new_factory(*args, **kwargs):
32
+ record = old_factory(*args, **kwargs)
33
+ req_id = request_id.get()
34
+ record.request_id = req_id
35
+ record.requestIdPrefix = prefix_format.format(request_id=req_id) if req_id else ""
36
+ return record
37
+
38
+ logging.setLogRecordFactory(new_factory)
39
+
40
+
41
+ class RequestIdAccessLogger(_AccessLogger):
42
+ """
43
+ Subclass of aiohttp.web_log.AccessLogger that sets the request_id
44
+ ContextVar while writing the access log line.
45
+
46
+ Needed because aiohttp writes the access log outside of the middleware
47
+ scope, where the ContextVar is already reset.
48
+
49
+ Usage: run_app(app, access_log_class=RequestIdAccessLogger)
50
+ """
51
+
52
+ def log(self, request: web.BaseRequest, response: web.StreamResponse, time: float) -> None:
53
+ try:
54
+ request_id_value = request[REQUEST_ID_KEY]
55
+ except KeyError:
56
+ # If there is no request[REQUEST_ID_KEY], for example when an error
57
+ # occurs in a middleware, fall back to just logging without setting
58
+ # the request_id context variable.
59
+ super().log(request, response, time)
60
+ return
61
+
62
+ token = request_id.set(request_id_value)
63
+ try:
64
+ super().log(request, response, time)
65
+ finally:
66
+ request_id.reset(token)
@@ -0,0 +1,398 @@
1
+ from aiohttp import web
2
+ from aiohttp.typedefs import Handler
3
+ from aiohttp.web_exceptions import HTTPException
4
+ from asyncio import CancelledError
5
+ from collections.abc import Callable
6
+ from contextlib import AbstractContextManager, ExitStack
7
+ from logging import getLogger
8
+ from typing import Any
9
+ import warnings
10
+
11
+ from .context import REQUEST_ID_KEY, FALLBACK_REQUEST_ID_KEY, request_id as request_id_cv
12
+ from .errors import RequestIdKeyAlreadySetError
13
+ from .request_id_factories import random_request_id_factory
14
+
15
+
16
+ logger = getLogger(__name__)
17
+
18
+
19
+ def noop(*args: Any, **kwargs: Any) -> None:
20
+ """
21
+ Pass this function as add_response_request_id_header or log_request_start to disable the default behavior.
22
+ """
23
+ pass
24
+
25
+
26
+ class RequestIdMiddleware:
27
+ """
28
+ aiohttp middleware that generates a request id for every request,
29
+ stores it in the request_id ContextVar and in the request
30
+ (request[REQUEST_ID_KEY]), adds an X-Request-Id response header,
31
+ and - if sentry_sdk is installed - creates a Sentry scope with
32
+ a request_id tag.
33
+
34
+ Constructor parameters (all keyword-only):
35
+
36
+ - request_id_factory: zero-argument callable returning the request id
37
+ string, used when get_request_id returns None (which the default
38
+ implementation always does); default: random_request_id_factory
39
+ - get_request_id: callable (request) returning the request id for the
40
+ given request, or None to have one generated with request_id_factory;
41
+ if you adopt an incoming header value here, validate it - it is
42
+ controlled by the client
43
+ - log_request_start: callable (request, handler) that logs the request
44
+ start message, replacing the default "Processing GET / (...)" one;
45
+ pass noop to disable the message
46
+ - log_function_name: include the handler name in the default request
47
+ start message; default: True
48
+ - add_response_request_id_header: callable (response, req_id) that adds
49
+ the request id header to the response, replacing the default behavior
50
+ (which keeps a header already set by the handler); pass noop to
51
+ disable the header
52
+ - request_id_header_name: name of the response header with the request
53
+ id; default: "X-Request-Id"
54
+ - no_fallback_request_id_key: if True, store the request id only under
55
+ REQUEST_ID_KEY and not under the backward compatibility plain string
56
+ key request['request_id']; default: False
57
+
58
+ The behavior can also be customized by subclassing - overriding the class
59
+ attributes (request_id_header_name, log_function_name) or the methods:
60
+ get_request_id, before_request, after_request,
61
+ get_response_for_exception, log_request_start, set_request_keys,
62
+ setup_sentry_scope, add_response_request_id_header, get_function_name.
63
+
64
+ Functions stored in class attributes are tricky (Python would bind them
65
+ as methods), that is why callables like request_id_factory are passed
66
+ via the constructor parameters instead.
67
+
68
+ The get_request_id, log_request_start and add_response_request_id_header
69
+ parameters take precedence over the methods of the same name - when the
70
+ parameter is passed, the method (even one overridden in a subclass) is
71
+ not called.
72
+
73
+ request_id_middleware is a backward compatibility wrapper function
74
+ creating an instance of this class.
75
+ """
76
+
77
+ __middleware_version__ = 1 # aiohttp 3 needs this; this is what @web.middleware is setting
78
+
79
+ # Default values - can be overridden when subclassing
80
+ # (plain values only - a function here would be bound as a method)
81
+ request_id_header_name: str = "X-Request-Id"
82
+ log_function_name: bool = True
83
+
84
+ def __init__(
85
+ self,
86
+ *,
87
+ request_id_factory: Callable[[], str] | None = None,
88
+ get_request_id: Callable[[web.Request], str | None] | None = None,
89
+ log_request_start: Callable[[web.Request, Handler], None] | None = None,
90
+ log_function_name: bool | None = None,
91
+ add_response_request_id_header: Callable[[web.StreamResponse, str], None] | None = None,
92
+ request_id_header_name: str | None = None,
93
+ no_fallback_request_id_key: bool = False,
94
+ ):
95
+ # Set self.request_id_factory
96
+ if request_id_factory is None:
97
+ self.request_id_factory = random_request_id_factory
98
+ elif callable(request_id_factory):
99
+ self.request_id_factory = request_id_factory
100
+ else:
101
+ raise TypeError("request_id_factory must be a callable")
102
+
103
+ # Set self._get_request_id_override, used instead of the get_request_id method
104
+ if get_request_id is not None and not callable(get_request_id):
105
+ raise TypeError("get_request_id must be a callable")
106
+ self._get_request_id_override = get_request_id
107
+
108
+ # Set self._log_request_start_override, used instead of the
109
+ # log_request_start method (see before_request)
110
+ if log_request_start is not None and not callable(log_request_start):
111
+ raise TypeError("log_request_start must be a callable; pass noop to disable the message")
112
+ self._log_request_start_override = log_request_start
113
+
114
+ # Set self.log_function_name
115
+ if log_function_name is not None:
116
+ self.log_function_name = log_function_name
117
+ if not isinstance(self.log_function_name, bool):
118
+ raise TypeError("log_function_name must be a bool")
119
+
120
+ # Set self._add_response_request_id_header_override, used instead of the
121
+ # add_response_request_id_header method (see after_request)
122
+ if add_response_request_id_header is not None and not callable(add_response_request_id_header):
123
+ raise TypeError("add_response_request_id_header must be a callable; pass noop to disable the header")
124
+ self._add_response_request_id_header_override = add_response_request_id_header
125
+
126
+ # Set self.request_id_header_name
127
+ if request_id_header_name is not None:
128
+ self.request_id_header_name = request_id_header_name
129
+ if not isinstance(self.request_id_header_name, str):
130
+ raise TypeError("request_id_header_name must be a str")
131
+
132
+ self._fallback_request_id_key = None if no_fallback_request_id_key else FALLBACK_REQUEST_ID_KEY
133
+
134
+ self.sentry_make_scope = self.resolve_sentry_make_scope()
135
+
136
+ async def __call__(self, request: web.Request, handler: Handler) -> web.StreamResponse:
137
+ """
138
+ The middleware entrypoint - process one request.
139
+
140
+ Obtains the request id from get_request_id (falling back to
141
+ request_id_factory when it returns None), sets the request_id
142
+ ContextVar for the whole duration of the request processing,
143
+ calls the handler between before_request and after_request,
144
+ and converts an unhandled exception from the handler to a 500
145
+ response (get_response_for_exception). A raised HTTPException
146
+ is re-raised for aiohttp to process - after after_request adds
147
+ the request id header to it.
148
+ """
149
+ if self._get_request_id_override is not None:
150
+ req_id = self._get_request_id_override(request)
151
+ else:
152
+ req_id = self.get_request_id(request)
153
+ if req_id is None:
154
+ req_id = self.request_id_factory()
155
+ with ExitStack() as stack:
156
+ # Set request id context variable as a first thing
157
+ token = request_id_cv.set(req_id)
158
+ stack.callback(lambda: request_id_cv.reset(token))
159
+
160
+ await self.before_request(request, handler, req_id, stack)
161
+
162
+ try:
163
+ response = await handler(request)
164
+ except CancelledError as exc:
165
+ logger.info("(Cancelled)")
166
+ raise exc
167
+ except HTTPException as http_exc:
168
+ # HTTPException is also the response aiohttp sends to the
169
+ # client, so let after_request add the request id header
170
+ # to it before re-raising. It must be re-raised, not
171
+ # returned - a returned HTTPException makes aiohttp emit
172
+ # the "returning HTTPException object is deprecated
173
+ # (#2415)" DeprecationWarning.
174
+ await self.after_request(request, handler, http_exc, req_id, stack)
175
+ raise http_exc
176
+ except Exception as exc:
177
+ # We are processing the 500 error right here, because if we
178
+ # let the web server process it, it would be outside of the
179
+ # request_id contextvar scope.
180
+ # (And also outside the sentry scope, if sentry is enabled.)
181
+ logger.exception("Error handling request: %r", exc)
182
+ response = self.get_response_for_exception(request, exc)
183
+
184
+ await self.after_request(request, handler, response, req_id, stack)
185
+ return response
186
+
187
+ def get_request_id(self, request: web.Request) -> str | None:
188
+ """
189
+ Return the request id for the given request, or None to have one
190
+ generated with request_id_factory.
191
+
192
+ The default implementation returns None. Override this to e.g.
193
+ adopt a request id from an incoming header - but validate the
194
+ value, it is controlled by the client
195
+ (see examples/demo_customization_subclassing.py).
196
+
197
+ Not called when a get_request_id callable was passed to the
198
+ constructor - the callable is used instead (see __call__).
199
+ """
200
+ return None
201
+
202
+ async def before_request(self, request: web.Request, handler: Handler, req_id: str, stack: ExitStack) -> None:
203
+ """
204
+ Called before the handler: store the request id in the request,
205
+ set up the Sentry scope and log the request start message.
206
+
207
+ The stack parameter (a contextlib.ExitStack) can be used to register
208
+ cleanup that runs after the request is processed.
209
+
210
+ This is where the log_request_start constructor parameter is applied -
211
+ when overriding this method without calling super(), taking the
212
+ parameter into account is up to you.
213
+ """
214
+ # Request keys come first so that the log_request_start hook can
215
+ # read request[REQUEST_ID_KEY], and so that a double-applied
216
+ # middleware raises RequestIdKeyAlreadySetError before producing
217
+ # any side effects (Sentry scope, log message).
218
+ self.set_request_keys(request, req_id)
219
+ # Sentry scope comes before the log message so that it is
220
+ # captured in the scope (as a breadcrumb).
221
+ self.setup_sentry_scope(req_id, stack)
222
+ if self._log_request_start_override is not None:
223
+ self._log_request_start_override(request, handler)
224
+ else:
225
+ self.log_request_start(request, handler)
226
+
227
+ def get_response_for_exception(self, request: web.Request, exc: Exception) -> web.StreamResponse:
228
+ """
229
+ Create the 500 response for an unhandled exception from the handler.
230
+
231
+ Override this to customize the error response - the request
232
+ parameter allows e.g. content negotiation (JSON for API paths).
233
+ """
234
+ response = web.Response(status=500, text="500 Internal Server Error\n")
235
+ response.force_close()
236
+ return response
237
+
238
+ async def after_request(
239
+ self,
240
+ request: web.Request,
241
+ handler: Handler,
242
+ response: web.StreamResponse,
243
+ req_id: str,
244
+ stack: ExitStack,
245
+ ) -> None:
246
+ """
247
+ Called after the handler returns - or with a raised HTTPException
248
+ before it is re-raised, or with the 500 response an unhandled
249
+ exception was converted to. Adds the request id response header.
250
+
251
+ This is where the add_response_request_id_header constructor
252
+ parameter is applied - when overriding this method without calling
253
+ super(), taking the parameter into account is up to you.
254
+ """
255
+ if self._add_response_request_id_header_override is not None:
256
+ self._add_response_request_id_header_override(response, req_id)
257
+ else:
258
+ self.add_response_request_id_header(response, req_id)
259
+
260
+ def log_request_start(self, request: web.Request, handler: Handler) -> None:
261
+ """
262
+ Log the "Processing GET / (...)" message at the start of the request.
263
+
264
+ Not called when a log_request_start callable was passed to the
265
+ constructor - the callable is used instead (see before_request).
266
+ """
267
+ if self.log_function_name:
268
+ logger.info("Processing %s %s (%s)", request.method, request.path, self.get_function_name(handler))
269
+ else:
270
+ logger.info("Processing %s %s", request.method, request.path)
271
+
272
+ def set_request_keys(self, request: web.Request, req_id: str) -> None:
273
+ """
274
+ Store the request id in the request - under REQUEST_ID_KEY and,
275
+ unless disabled (no_fallback_request_id_key=True), also under
276
+ the backward compatibility plain string key request['request_id'].
277
+ """
278
+ if REQUEST_ID_KEY in request:
279
+ raise RequestIdKeyAlreadySetError(request[REQUEST_ID_KEY])
280
+ request[REQUEST_ID_KEY] = req_id
281
+
282
+ if self._fallback_request_id_key is not None:
283
+ with warnings.catch_warnings():
284
+ warnings.simplefilter("ignore", getattr(web, "NotAppKeyWarning", UserWarning))
285
+
286
+ if self._fallback_request_id_key in request:
287
+ raise RequestIdKeyAlreadySetError(request[self._fallback_request_id_key])
288
+ request[self._fallback_request_id_key] = req_id
289
+
290
+ @staticmethod
291
+ def resolve_sentry_make_scope() -> Callable[[], AbstractContextManager[Any]] | None:
292
+ """
293
+ Find the function for creating a new Sentry scope, or return None
294
+ if sentry_sdk is not installed (or provides no such function).
295
+
296
+ For compatibility with sentry_sdk 1.x and 2.x:
297
+ push_scope is deprecated and will be removed, isolation_scope is its
298
+ recommended replacement for the request-response cycle.
299
+ """
300
+ # Read sentry_sdk from the package at call time so that
301
+ # a replaced aiohttp_request_id_logging.sentry_sdk (e.g. monkeypatched
302
+ # in tests) is taken into account.
303
+ from . import sentry_sdk
304
+
305
+ if sentry_sdk is None:
306
+ return None
307
+ try:
308
+ return sentry_sdk.isolation_scope
309
+ except AttributeError:
310
+ pass
311
+ try:
312
+ return sentry_sdk.push_scope
313
+ except AttributeError:
314
+ warnings.warn(
315
+ "sentry_sdk does not contain isolation_scope or push_scope. "
316
+ "This is most likely due to a version change to >2.x, "
317
+ "please consult the Sentry documentation on how to fix this. "
318
+ "The `request_id` tag will not be pushed to Sentry.",
319
+ UserWarning,
320
+ )
321
+ return None
322
+
323
+ def setup_sentry_scope(self, req_id: str, stack: ExitStack) -> None:
324
+ """
325
+ Create a new Sentry scope (entered into the given ExitStack)
326
+ and add the request_id tag to it.
327
+
328
+ Does nothing if sentry_sdk is not installed.
329
+ """
330
+ if self.sentry_make_scope is not None:
331
+ scope = stack.enter_context(self.sentry_make_scope())
332
+ scope.set_tag("request_id", req_id)
333
+
334
+ def add_response_request_id_header(self, response: web.StreamResponse, req_id: str) -> None:
335
+ """
336
+ Add the request id response header (named by request_id_header_name).
337
+
338
+ The header is silently not added (a missing response request id
339
+ header is not a serious problem) when:
340
+
341
+ - the response already contains the header, for example the handler
342
+ echoes the request id of an upstream proxy,
343
+ - the response was already prepared (a streaming or WebSocket
344
+ handler called response.prepare()) - its headers were already
345
+ sent to the client and cannot be changed anymore; to have the
346
+ header on streaming responses, set it in the handler before
347
+ calling prepare().
348
+
349
+ Not called when an add_response_request_id_header callable was
350
+ passed to the constructor - the callable is used instead
351
+ (see after_request).
352
+ """
353
+ try:
354
+ if response.prepared:
355
+ # Mutating response.headers now would succeed, but the change
356
+ # would never reach the client.
357
+ return
358
+ if self.request_id_header_name in response.headers:
359
+ return
360
+ response.headers[self.request_id_header_name] = req_id
361
+ except Exception as e:
362
+ # Let's consider this response header non critical
363
+ logger.debug("Could not set response.headers[%r]: %r", self.request_id_header_name, e)
364
+
365
+ @staticmethod
366
+ def get_function_name(f: Callable[..., Any]) -> str:
367
+ """
368
+ Return a human-readable handler name for the request start message.
369
+ """
370
+ try:
371
+ return f"{f.__module__}:{f.__name__}" # ty: ignore[unresolved-attribute]
372
+ except Exception:
373
+ return str(f)
374
+
375
+
376
+ def request_id_middleware(
377
+ *,
378
+ request_id_factory: Callable[[], str] | None = None,
379
+ log_function_name: bool = True,
380
+ log_request_start: bool = True,
381
+ ) -> RequestIdMiddleware:
382
+ """
383
+ Backward compatibility function for creating a RequestIdMiddleware instance.
384
+
385
+ Unlike the RequestIdMiddleware constructor, log_request_start is a bool
386
+ here - log_request_start=False translates to log_request_start=noop.
387
+ """
388
+ if request_id_factory is not None and not callable(request_id_factory):
389
+ raise TypeError("request_id_factory must be a callable")
390
+ if not isinstance(log_function_name, bool):
391
+ raise TypeError("log_function_name must be a bool")
392
+ if not isinstance(log_request_start, bool):
393
+ raise TypeError("log_request_start must be a bool here; the RequestIdMiddleware constructor takes a callable")
394
+ return RequestIdMiddleware(
395
+ request_id_factory=request_id_factory,
396
+ log_function_name=log_function_name,
397
+ log_request_start=None if log_request_start else noop,
398
+ )
File without changes
@@ -0,0 +1,74 @@
1
+ from os import getpid
2
+ from secrets import token_urlsafe
3
+
4
+
5
+ _SKIP_CHARS = "1lI2ZO0"
6
+
7
+
8
+ def random_request_id_factory(length: int = 7) -> str:
9
+ """
10
+ Generate a random request id - a URL-safe string of the given length.
11
+
12
+ Visually ambiguous characters (1/l/I, 2/Z, O/0) and the URL-safe
13
+ punctuation (_, -) are never used in the generated id.
14
+
15
+ This is the default request id factory used in RequestIdMiddleware.
16
+ """
17
+ while True:
18
+ req_id = token_urlsafe(length)[:length]
19
+ if "_" in req_id or "-" in req_id:
20
+ continue
21
+ if any(c in _SKIP_CHARS for c in req_id):
22
+ continue
23
+ return req_id
24
+
25
+
26
+ class SequentialRequestIdFactory:
27
+ """
28
+ Alternative request id factory producing ids like "Wxyz0000", "Wxyz0001"...
29
+ - a random per-process prefix followed by a sequential number.
30
+
31
+ Usage: RequestIdMiddleware(request_id_factory=sequential_request_id_factory)
32
+
33
+ Caveat: if the request ids are ever exposed to clients (response header,
34
+ error page...), sequential ids reveal how many requests the server
35
+ processes and how many server processes there are. Note that
36
+ RequestIdMiddleware sends the request id to clients in the X-Request-Id
37
+ response header by default - pass add_response_request_id_header=noop
38
+ to disable that. If the exposure is a concern, use the default
39
+ random_request_id_factory instead.
40
+ """
41
+
42
+ prefix_length: int = 4
43
+
44
+ def __init__(self):
45
+ self._pid: int | None = None
46
+ self._prefix: str | None = None
47
+ self._next_value: int = 0
48
+
49
+ def __call__(self) -> str:
50
+ pid = getpid()
51
+ if pid != self._pid:
52
+ self._prefix = self._generate_prefix()
53
+ self._next_value = 0
54
+ self._pid = pid
55
+ value = self._next_value
56
+ self._next_value += 1
57
+ return f"{self._prefix}{value:04}"
58
+
59
+ @classmethod
60
+ def _generate_prefix(cls) -> str:
61
+ while True:
62
+ prefix = token_urlsafe(cls.prefix_length)[: cls.prefix_length]
63
+ if "_" in prefix or "-" in prefix:
64
+ continue
65
+ # Let's not have any numbers in the prefix so we keep more focus on the appended request number.
66
+ # This is just aesthetic thing.
67
+ if any(c.isdigit() for c in prefix):
68
+ continue
69
+ if any(c in _SKIP_CHARS for c in prefix):
70
+ continue
71
+ return prefix
72
+
73
+
74
+ sequential_request_id_factory = SequentialRequestIdFactory()
@@ -0,0 +1,487 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiohttp-request-id-logging
3
+ Version: 1.0.0
4
+ Summary: Setup proper request id logging for your Aiohttp app
5
+ Maintainer-email: Petr Messner <petr.messner@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/messa/aiohttp-request-id-logging
8
+ Project-URL: Bug Reports, https://github.com/messa/aiohttp-request-id-logging/issues
9
+ Project-URL: Source, https://github.com/messa/aiohttp-request-id-logging
10
+ Keywords: aiohttp,logging
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Framework :: aiohttp
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: aiohttp
26
+ Dynamic: license-file
27
+
28
+ aiohttp-request-id-logging
29
+ ==========================
30
+
31
+ Motivation
32
+ ----------
33
+
34
+ When logging from an async application (e.g. aiohttp web application),
35
+ log messages from different async tasks, http requests etc. are intertwined and you cannot surely tell which line was generated by what request.
36
+ For example:
37
+
38
+ ```
39
+ 2020-01-15 15:35:37,501 INFO: Processing transfer id 1234
40
+ 2020-01-15 15:35:37,976 INFO: Processing transfer id 5678
41
+ 2020-01-15 15:35:38,201 ERROR: Oh no, something bad has happened! Cannot finish the transfer.
42
+ 2020-01-15 15:35:38,504 INFO: 127.0.0.1 [15/Jan/2020:14:35:36 +0000] "POST / HTTP/1.1" 200 165 "-" "curl/7.68.0"
43
+ 2020-01-15 15:35:38,982 INFO: 127.0.0.1 [15/Jan/2020:14:35:36 +0000] "POST / HTTP/1.1" 500 165 "-" "curl/7.68.0"
44
+ ```
45
+
46
+ So, which transfer has failed? The one with id 1234, or the one with id 5678?
47
+
48
+ Of course, ideally the error message should contain also the transfer id.
49
+ But it is not always this case, or you may want to inspect also other log messages that were logged by that specific task/request.
50
+
51
+ When you start to use this library in your aiohttp web application, this is how your log messages will look like:
52
+
53
+ ```
54
+ 2020-01-15 15:58:47,238 INFO: [req:kbyLG8d] Processing GET / (__main__:hello)
55
+ 2020-01-15 15:58:47,950 INFO: [req:xtMacpA] Processing GET / (__main__:hello)
56
+ 2020-01-15 15:58:48,240 INFO: [req:kbyLG8d] Processing transfer id 1234
57
+ 2020-01-15 15:58:48,953 INFO: [req:xtMacpA] Processing transfer id 5678
58
+ 2020-01-15 15:58:49,182 ERROR: [req:xtMacpA] Oh no, something bad has happened! Cannot finish the transfer.
59
+ 2020-01-15 15:58:49,242 INFO: [req:kbyLG8d] 127.0.0.1 "GET / HTTP/1.1" 200 165 "-" "curl/7.68.0"
60
+ 2020-01-15 15:58:49,959 INFO: [req:xtMacpA] 127.0.0.1 "GET / HTTP/1.1" 500 165 "-" "curl/7.68.0"
61
+ ```
62
+
63
+
64
+ Installation
65
+ ------------
66
+
67
+ ```shell
68
+ $ uv add https://github.com/messa/aiohttp-request-id-logging/archive/v1.0.0.zip
69
+ # or
70
+ $ python3 -m pip install https://github.com/messa/aiohttp-request-id-logging/archive/v1.0.0.zip
71
+ ```
72
+
73
+ Or add this line to `requirements.txt`:
74
+
75
+ ```
76
+ aiohttp-request-id-logging @ https://github.com/messa/aiohttp-request-id-logging/archive/v1.0.0.zip
77
+ ```
78
+
79
+
80
+ Usage
81
+ -----
82
+
83
+ ```python
84
+ from aiohttp.web import Application, Response, RouteTableDef, run_app
85
+ from logging import DEBUG, basicConfig
86
+ from aiohttp_request_id_logging import (
87
+ setup_logging_request_id_prefix,
88
+ RequestIdMiddleware,
89
+ RequestIdAccessLogger,
90
+ )
91
+
92
+ routes = RouteTableDef()
93
+
94
+ @routes.get('/')
95
+ async def hello(request):
96
+ return Response(text="Hello, world!\n")
97
+
98
+ basicConfig(
99
+ level=DEBUG,
100
+ format='%(asctime)s [%(threadName)s] %(name)-37s %(levelname)5s: %(requestIdPrefix)s%(message)s')
101
+
102
+ setup_logging_request_id_prefix()
103
+
104
+ app = Application(middlewares=[RequestIdMiddleware()])
105
+ app.router.add_routes(routes)
106
+
107
+ run_app(app, access_log_class=RequestIdAccessLogger)
108
+ ```
109
+
110
+ For more complete example see [examples/demo.py](examples/demo.py).
111
+
112
+ Customization of the middleware behavior is demonstrated in
113
+ [`examples/demo_customization_injection.py`](examples/demo_customization_injection.py)
114
+ (constructor parameters) and
115
+ [`examples/demo_customization_subclassing.py`](examples/demo_customization_subclassing.py)
116
+ (subclassing).
117
+
118
+
119
+ How it works
120
+ ------------
121
+
122
+ This library helps you to add request (correlation) id to the log messages in a few steps:
123
+
124
+ 1. **`RequestIdMiddleware`:** Generate random `request_id` for each aiohttp request and
125
+
126
+ - store it in a ContextVar `aiohttp_request_id_logging.request_id`
127
+ - store it also in the request storage: `request[aiohttp_request_id_logging.REQUEST_ID_KEY]`
128
+ (and, by default, also under the plain string key `request['request_id']` for backward compatibility)
129
+ - return it to the client in the `X-Request-Id` response header
130
+
131
+ 2. **`setup_logging_request_id_prefix()`:** Modify logging record factory so that the `request_id` is attached to every logging record created
132
+
133
+ - so you should modify your log format, for example `logging.basicConfig(format=... %(levelname)5s: %(requestIdPrefix)s%(message)s')`
134
+
135
+ 3. Because the aiohttp access logging happens out of the middleware scope, the request id ContextVar would be already resetted. So **`RequestIdAccessLogger`** is provided that adds the request_id to the access log message.
136
+
137
+ 4. Unhandled exceptions ("500 errors") are logged and converted to a 500 response directly by **`RequestIdMiddleware`** – if they were left to the standard aiohttp error handling, the traceback would be logged outside of the middleware scope, without the request id (see the [Reference](#requestidmiddleware) for details).
138
+
139
+ 5. If you use **[Sentry](https://docs.sentry.io/platforms/python/aiohttp/)**, a `request_id` [tag](https://docs.sentry.io/enriching-error-data/context/?platform=python#tagging-events) is added when the request is processed.
140
+
141
+ Sentry integration will be active only if you have `sentry_sdk` installed.
142
+
143
+
144
+ Reference
145
+ ---------
146
+
147
+ All names are importable directly from the `aiohttp_request_id_logging` package.
148
+
149
+ ### `RequestIdMiddleware`
150
+
151
+ The aiohttp middleware. Generates a request id for every request, stores it
152
+ in the `request_id` ContextVar and in the request, adds an `X-Request-Id`
153
+ response header, and (if Sentry is installed) creates a Sentry scope with
154
+ a `request_id` tag.
155
+
156
+ Exceptions raised from the handler are processed by the middleware too:
157
+
158
+ - an `HTTPException` (the aiohttp way of returning error responses from
159
+ anywhere in the handler) is re-raised for aiohttp to process, with the
160
+ request id response header added to it first
161
+ - any other unhandled exception is logged together with its traceback and
162
+ converted to a plain text `500 Internal Server Error` response (also
163
+ carrying the request id header) right in the middleware
164
+
165
+ The latter is deliberate: if the exception were left to the standard aiohttp
166
+ error handling, it would be logged outside of the middleware scope, where the
167
+ `request_id` ContextVar is already reset (and the Sentry scope with the
168
+ `request_id` tag closed) – so the traceback, usually the log message you need
169
+ to pair with a request the most, would be the one line without the request id.
170
+ Override `get_response_for_exception(request, exc)` to customize the error
171
+ response (a nicer error page, JSON for API paths...).
172
+
173
+ Constructor parameters (all keyword-only):
174
+
175
+ - `request_id_factory` – a zero-argument callable returning the request id string,
176
+ used when `get_request_id` returns `None` (which the default implementation
177
+ always does); default: `random_request_id_factory`
178
+ - `get_request_id` – a callable `(request)` returning the request id for the
179
+ given request, or `None` to have one generated with `request_id_factory`;
180
+ if you adopt an incoming header value here, validate it – it is controlled
181
+ by the client
182
+ - `log_request_start` – a callable `(request, handler)` that logs the request
183
+ start message, replacing the default `Processing GET / (...)` one;
184
+ pass `noop` to disable the message
185
+ - `log_function_name` – include the handler name in the default request start
186
+ message; default: `True`
187
+ - `add_response_request_id_header` – a callable `(response, req_id)` that adds
188
+ the request id header to the response, replacing the default behavior
189
+ (which keeps a header already set by the handler); pass `noop` to disable
190
+ the header
191
+ - `request_id_header_name` – name of the response header with the request id;
192
+ default: `X-Request-Id`
193
+ - `no_fallback_request_id_key` – if `True`, the request id is stored in the request
194
+ only under `REQUEST_ID_KEY`, not also under the backward compatibility plain
195
+ string key `request['request_id']`; default: `False`
196
+
197
+ The behavior can also be customized by subclassing – overriding the class
198
+ attributes (`request_id_header_name`, `log_function_name`) or the methods:
199
+ `get_request_id`, `before_request`, `after_request`,
200
+ `get_response_for_exception`, `log_request_start`, `set_request_keys`,
201
+ `setup_sentry_scope`, `add_response_request_id_header`, `get_function_name`.
202
+
203
+ Both approaches are demonstrated in
204
+ [`examples/demo_customization_injection.py`](examples/demo_customization_injection.py) and
205
+ [`examples/demo_customization_subclassing.py`](examples/demo_customization_subclassing.py).
206
+
207
+ Functions stored in class attributes are tricky (Python would bind them
208
+ as methods), that is why callables like `request_id_factory` are passed
209
+ via the constructor parameters instead.
210
+
211
+ The `get_request_id`, `log_request_start` and `add_response_request_id_header`
212
+ parameters take precedence over the methods of the same name – when the
213
+ parameter is passed, the method (even one overridden in a subclass) is not
214
+ called.
215
+
216
+ The middleware does not adopt a request id sent by the client – how (and
217
+ whether) to trust such a value depends on the deployment. If you want that,
218
+ pass (or override) `get_request_id(request)` and validate the incoming value
219
+ there; see
220
+ [`examples/demo_customization_subclassing.py`](examples/demo_customization_subclassing.py).
221
+
222
+ `request_id_middleware()` is a backward compatibility wrapper of this class;
223
+ unlike the constructor, its `log_request_start` parameter is a bool –
224
+ `log_request_start=False` translates to `log_request_start=noop`.
225
+
226
+ ### `setup_logging_request_id_prefix()`
227
+
228
+ Wraps the logging record factory so that every log record gets two extra
229
+ attributes usable in the log format:
230
+
231
+ - `%(requestIdPrefix)s` – `"[req:kbyLG8d] "`, or an empty string outside of a request
232
+ - `%(request_id)s` – the raw request id, or `None`
233
+
234
+ The prefix can be customized with the `prefix_format` parameter
235
+ (default: `"[req:{request_id}] "`).
236
+
237
+ Safe to call multiple times (does the setup only once).
238
+
239
+ ### `RequestIdAccessLogger`
240
+
241
+ Subclass of `aiohttp.web_log.AccessLogger` that sets the `request_id`
242
+ ContextVar while writing the access log line. Needed because aiohttp writes
243
+ the access log outside of the middleware scope. Pass it to
244
+ `run_app(app, access_log_class=RequestIdAccessLogger)`.
245
+
246
+ ### `request_id`
247
+
248
+ `ContextVar` holding the request id of the currently processed request.
249
+ Read it with `request_id.get()` – it returns `None` outside of a request.
250
+
251
+ ### `REQUEST_ID_KEY`
252
+
253
+ Key under which the request id is stored in the request:
254
+ `request[REQUEST_ID_KEY]`. It is a `web.RequestKey` instance on aiohttp
255
+ versions that support it, otherwise the plain string `'request_id'`.
256
+ (The plain string key `request['request_id']` is set as well by default,
257
+ for backward compatibility; pass `RequestIdMiddleware(no_fallback_request_id_key=True)`
258
+ to store the id only under `REQUEST_ID_KEY`.)
259
+
260
+ `FALLBACK_REQUEST_ID_KEY` is that backward compatibility plain string key –
261
+ `'request_id'`, or `None` on aiohttp versions without `web.RequestKey`
262
+ (there `REQUEST_ID_KEY` itself is the plain string, so there is no fallback).
263
+
264
+ ### Request id factories
265
+
266
+ - `random_request_id_factory(length=7)` – the default; returns a random URL-safe
267
+ string of the given length, avoiding visually ambiguous characters
268
+ (`1`/`l`/`I`, `2`/`Z`, `O`/`0`)
269
+ (`generate_request_id` is its backward compatibility alias)
270
+ - `sequential_request_id_factory` / `SequentialRequestIdFactory` – alternative
271
+ factory producing ids like `Wxyz0000`, `Wxyz0001`… – a random per-process
272
+ prefix followed by a sequential number
273
+
274
+ Usage: `RequestIdMiddleware(request_id_factory=sequential_request_id_factory)`
275
+
276
+ Caveat: if the request ids are ever exposed to clients (response header,
277
+ error page…), sequential ids reveal how many requests the server processes
278
+ and how many server processes there are. Note that the middleware sends the
279
+ request id in the `X-Request-Id` response header by default – pass
280
+ `add_response_request_id_header=noop` to disable that. If the exposure is
281
+ a concern, stick with the default random factory.
282
+
283
+ ### `noop`
284
+
285
+ A do-nothing function. Pass it as the `log_request_start` or
286
+ `add_response_request_id_header` constructor parameter to disable
287
+ the corresponding default behavior.
288
+
289
+ ### `RequestIdKeyAlreadySetError`
290
+
291
+ Raised by the middleware when the request already contains a request id –
292
+ most likely the middleware is applied twice, or something else sets it too.
293
+ The existing id is available as the `existing_request_id` attribute.
294
+
295
+
296
+ Development
297
+ -----------
298
+
299
+ The project uses [uv](https://docs.astral.sh/uv/) for dependency and environment
300
+ management, [Ruff](https://docs.astral.sh/ruff/) for linting and formatting
301
+ and [ty](https://docs.astral.sh/ty/) for type checking.
302
+
303
+ ```shell
304
+ $ make check # lint + ty check + tests (pytest)
305
+ $ make lint # ruff check + ruff format --check
306
+ $ make lint-fix # apply ruff fixes and formatting
307
+ $ make typecheck # ty check
308
+ ```
309
+
310
+ The Makefile targets use `uv run`, which automatically creates the `.venv`
311
+ virtualenv and installs the dependencies (including the `dev` dependency group)
312
+ on the first run.
313
+
314
+
315
+ Version changelog
316
+ -----------------
317
+
318
+ ### 1.0.0 (2026-07-16)
319
+
320
+ - Added `aiohttp_request_id_logging.__version__`
321
+ - `RequestIdContextAccessLogger` was renamed to `RequestIdAccessLogger`;
322
+ the old name still works as a backward compatibility alias
323
+ - The middleware was refactored into a class `RequestIdMiddleware` so that it can be
324
+ subclassed and individual parts of the behavior customized by overriding class
325
+ attributes and methods (`before_request`, `after_request`,
326
+ `log_request_start`, `set_request_keys`, `setup_sentry_scope`,
327
+ `add_response_request_id_header`...)
328
+ - `request_id_middleware()` is kept as a backward compatibility wrapper function
329
+ - the deprecated-in-aiohttp-4 `@web.middleware` decorator is no longer used
330
+ - see the new examples
331
+ [`examples/demo_customization_injection.py`](examples/demo_customization_injection.py) and
332
+ [`examples/demo_customization_subclassing.py`](examples/demo_customization_subclassing.py)
333
+ - The middleware now returns the request id to the client in the `X-Request-Id`
334
+ response header (a header already set by the handler is not overwritten);
335
+ the header name can be changed with the `request_id_header_name`
336
+ parameter, or the header disabled completely with `add_response_request_id_header=noop`
337
+ - New hook `get_request_id(request)` (a constructor parameter or an overridable
338
+ method) – returns the request id for the given request, e.g. adopted from
339
+ an incoming header, or `None` to have a new id generated with
340
+ `request_id_factory` (if you adopt an incoming value, validate it – see
341
+ [`examples/demo_customization_subclassing.py`](examples/demo_customization_subclassing.py))
342
+ - The response request id header is silently skipped for responses that
343
+ were already prepared (streaming/WebSocket handlers)
344
+ - The request start message ("Processing ...") can be disabled
345
+ (`request_id_middleware(log_request_start=False)`,
346
+ `RequestIdMiddleware(log_request_start=noop)`) or replaced with a custom callable
347
+ (`RequestIdMiddleware(log_request_start=my_log_function)`)
348
+ - The middleware now raises `RequestIdKeyAlreadySetError` when the request
349
+ already contains a request id, for example when the middleware is applied twice
350
+ or something else also sets the request id
351
+ - New parameter `RequestIdMiddleware(no_fallback_request_id_key=True)` stores the
352
+ request id only under `REQUEST_ID_KEY`, skipping the backward compatibility
353
+ plain string key `request['request_id']` (the plain string key is now also
354
+ exported as `FALLBACK_REQUEST_ID_KEY`)
355
+ - Breaking: all `request_id_middleware()` parameters are now keyword-only; positional calls
356
+ like `request_id_middleware(my_factory)` raise `TypeError` – use
357
+ `request_id_middleware(request_id_factory=my_factory)` instead
358
+ - `HTTPException` raised from a handler is re-raised as before, but the request id
359
+ response header is now added to it first (the exception is also the response
360
+ aiohttp sends to the client)
361
+ - The 500 response for an unhandled exception from the handler can be customized
362
+ by overriding the `get_response_for_exception(request, exc)` method
363
+ - New parameter `setup_logging_request_id_prefix(prefix_format=...)` customizes the log
364
+ record prefix (default: `"[req:{request_id}] "`)
365
+ - The `request_id` ContextVar now has a default of `None`, so it can be read
366
+ with just `request_id.get()` – no more `LookupError` (or the `request_id.get(None)`
367
+ workaround) outside of a request
368
+ - Breaking: `random_request_id_factory()` has a new `length` parameter, replacing the
369
+ removed module-level variables `request_id_default_length` and
370
+ `default_request_id_factory`; the module-level function `get_function_name`
371
+ was also removed (it is now a `RequestIdMiddleware` static method)
372
+ - Generated request ids now avoid visually ambiguous characters
373
+ (`1`/`l`/`I`, `2`/`Z`, `O`/`0`)
374
+ - The package was split into more modules (`middleware.py`, `errors.py`, `context.py`,
375
+ `logging_setup.py`, `request_id_factories.py`); everything is
376
+ still importable directly from `aiohttp_request_id_logging`, which now also defines `__all__`
377
+ - Added type hints, together with the `py.typed` marker so that the types
378
+ are visible to type checkers (mypy, pyright) in projects using this library
379
+ - Added example [examples/demo_legacy.py](examples/demo_legacy.py) demonstrating
380
+ the backward compatible `request_id_middleware()` usage
381
+ - Tests: every example in [examples/](examples/) is now run and checked
382
+ (the request id in the response header, the log line prefixes, clean stderr);
383
+ added tests for the HTTPException and 500 error handling
384
+ - Development: the project is now managed with [uv](https://docs.astral.sh/uv/)
385
+ (`uv.lock`, dev dependencies in the `[dependency-groups]` table, which replaced
386
+ the `test` extra); linting was migrated from flake8 to
387
+ [Ruff](https://docs.astral.sh/ruff/) and the codebase was reformatted
388
+ with `ruff format` (see the new `make lint` and `make lint-fix` targets)
389
+ - Development: added type checking with [ty](https://docs.astral.sh/ty/)
390
+ (`make typecheck`, also run in CI); `sentry-sdk` was added to the dev
391
+ dependencies so that the Sentry integration is type checked too
392
+ - Packaging: modernized the pyproject metadata – SPDX license expression
393
+ (`license = "MIT"`), added the Development Status, Framework and
394
+ Typing :: Typed classifiers
395
+ - Github Actions: lint runs in a separate job; uv is used to install Python
396
+ and the dependencies; the test matrix also includes the latest aiohttp
397
+ release (unpinned) to catch regressions with new aiohttp versions early
398
+
399
+ ### 0.0.8 (2026-07-14)
400
+
401
+ - Store the request id in the request under a `web.RequestKey` instance,
402
+ exported as `aiohttp_request_id_logging.REQUEST_ID_KEY` – fixes `NotAppKeyWarning`
403
+ emitted by aiohttp 3.13/3.14
404
+ - the plain string key `request['request_id']` is still set for backward compatibility
405
+ - Moved `demo.py` to [examples/](examples/)
406
+ - Fixed pyproject metadata
407
+ - Tests: run the demo in Python dev mode and assert clean stderr
408
+ (aiohttp-internal warnings are whitelisted)
409
+ - Github Actions: added Python 3.14, updated action versions
410
+
411
+ ### 0.0.7 (2025-05-12)
412
+
413
+ - Support new scope handling (`isolation_scope`) in sentry_sdk 2.x
414
+ and removed an incorrect upgrade warning
415
+ ([#6](https://github.com/messa/aiohttp-request-id-logging/pull/6), thanks @martinhanzik)
416
+ - Github Actions: added Python 3.13
417
+
418
+ ### 0.0.6 (2024-06-02)
419
+
420
+ - Added `sequential_request_id_factory` as an alternative request id generator
421
+ - Improved `demo.py` and added a test running it
422
+
423
+ ### 0.0.5 (2024-05-02)
424
+
425
+ - `RequestIdContextAccessLogger` no longer fails when the request id is not set,
426
+ for example when an error occurs in a middleware
427
+ - Github Actions: added Python 3.12
428
+
429
+ ### 0.0.4 (2023-05-11)
430
+
431
+ - Fixed `request_id_default_length` handling: the generated request id is now
432
+ exactly that many characters long (and the default was changed from 5 to 7)
433
+
434
+ ### 0.0.3 (2023-05-11)
435
+
436
+ - Added `req:` to the log record prefix: `[x12y3]` → `[req:x12y3]`
437
+
438
+ ### 0.0.2 (2023-05-11)
439
+
440
+ - Migrated packaging from `setup.py` to `pyproject.toml`
441
+ - Added flake8 lint configuration
442
+ - Github Actions: added Python 3.10 and 3.11, added aiohttp version matrix
443
+
444
+ ### 0.0.1 (2021-05-18)
445
+
446
+ - Initial release: `request_id_middleware()`, `setup_logging_request_id_prefix()`,
447
+ `RequestIdContextAccessLogger` and the Sentry `request_id` tag integration
448
+
449
+
450
+ Alternatives
451
+ ------------
452
+
453
+ - [belvaio-request-id](https://pypi.org/project/belvaio-request-id/) – another
454
+ request id middleware + access logger for aiohttp; it also adopts the id
455
+ from the incoming `X-Request-Id` header and attaches it to log records via
456
+ a logging filter (instead of the log record factory used here); last
457
+ released in 2021 and its source repository is no longer available
458
+ - [asgi-correlation-id](https://github.com/snok/asgi-correlation-id) – the
459
+ same idea for the ASGI world (FastAPI, Starlette, Django...; aiohttp is not
460
+ ASGI, so it cannot be used here) – correlation id middleware, a logging
461
+ filter and a Sentry integration
462
+ - [django-guid](https://github.com/snok/django-guid) – correlation id
463
+ middleware and logging integration for Django
464
+ - [structlog contextvars](https://www.structlog.org/en/stable/contextvars.html) –
465
+ if you log via structlog, you can bind a request id to the context-local
466
+ context yourself (`structlog.contextvars.bind_contextvars(request_id=...)`)
467
+ in a small custom middleware
468
+ - [aiotask-context](https://github.com/Skyscanner/aiotask-context) – stores
469
+ contextual data (e.g. a request id) directly in the asyncio task and
470
+ includes an aiohttp request id logging example; predates `contextvars`,
471
+ its last release was in 2020 and the repository is now archived
472
+ - [OpenTelemetry Python](https://opentelemetry.io/docs/languages/python/) –
473
+ the heavyweight solution: full distributed tracing with an aiohttp server
474
+ instrumentation, and the logging instrumentation can optionally inject
475
+ `otelTraceID`/`otelSpanID` into log records (using the same log record
476
+ factory technique as this library)
477
+
478
+
479
+ Links
480
+ -----
481
+
482
+ In general, you should be familiar with Aiohttp documentation related to production deployment:
483
+
484
+ - https://docs.aiohttp.org/en/stable/web_advanced.html#deploying-behind-a-proxy
485
+ - https://docs.aiohttp.org/en/stable/deployment.html
486
+
487
+ This is where this started: https://stackoverflow.com/a/58801740/196206
@@ -0,0 +1,12 @@
1
+ aiohttp_request_id_logging/__init__.py,sha256=WOSJkJZINkxmqgNTA56YvHWnMs8Q06EOVa6xvpzM9i4,1935
2
+ aiohttp_request_id_logging/context.py,sha256=FegKTEIb8uaSZu4CegXoK3OVXRxJmF9OlX0_FjDNgp0,737
3
+ aiohttp_request_id_logging/errors.py,sha256=g6TKVjc1KNZP5XovBlxsPt4RMMPGCqmTHyVzSt_NSwI,610
4
+ aiohttp_request_id_logging/logging_setup.py,sha256=wDzBLcXlfM1Pbp6Ubue_HlqhhrrvrxoUd_dEmlnD6ys,2420
5
+ aiohttp_request_id_logging/middleware.py,sha256=Hgu8CEl9qTFbEK2SQ2FLCkxt_JdeNCnvHsuzBeSV4Do,18115
6
+ aiohttp_request_id_logging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ aiohttp_request_id_logging/request_id_factories.py,sha256=XOTpPbLCS5KMrjNxyDb0_hcpYFz6CsTj9Efnxws8i5U,2487
8
+ aiohttp_request_id_logging-1.0.0.dist-info/licenses/LICENSE,sha256=_bbzd693JgJttwfk-gFGTwJiLY49xqJsRbLp36PBIyw,1069
9
+ aiohttp_request_id_logging-1.0.0.dist-info/METADATA,sha256=PHT-U3Lis2zqwFdH1Dh94oXLdlSll-1YVfOTf3nk-DE,23209
10
+ aiohttp_request_id_logging-1.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
11
+ aiohttp_request_id_logging-1.0.0.dist-info/top_level.txt,sha256=UCvVq_jfTvfEg9Z46kyiqmuQATFsTTTP2QEg_M8srLA,27
12
+ aiohttp_request_id_logging-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Petr Messner
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ aiohttp_request_id_logging