Features
Context
Sometimes, you may want to store and share data throughout requests or jobs/messages, like the authenticated user or request ID. Expanse provides a context object that you can use to share this information and make it available automatically to log messages and jobs dispatched during a particular request or command, allowing you to trace easily the execution of your code and have better insights into the behavior of your application.
Using the context
To start using the context, you can inject a Context instance anywhere depedency injection is supported, such as in controllers, middleware, jobs, message handlers and beam commands.
For this example, let's say you want to store the ID of the request via a middleware so you can trace it in your log messages:
from expanse.http.request import Request
from expanse.logging.context import Context
from expanse.types.http.middleware import RequestHandler
class IdentifyRequest:
def __init__(self, context: Context) -> None:
self._context: Context = context
async def handle(self, request: Request, next_call: RequestHandler):
request_id = request.headers.get("X-Request-ID")
if request_id is None:
import uuid
request_id = str(uuid.uuid4())
self._context["request.id"] = request_id
response = await next_call(request)
response.headers["X-Request-ID"] = request_id
return response
From now on and for the duration of the request, the request.id value will be available in the context and can be used in your log messages or jobs dispatched during that request. So if you were to add the following log message in your controller:
logger.info("Processing request")
The context would automatically be added to the log message as contextual data and would look like this in your terminal:
12:34:56 INFO Processing request context: {"request": {"id": "123e4567-e89b-12d3-a456-426614174000"}}
If you have configured your channels to use structured log messages, the context will be included in the JSON log message as well:
{
"timestamp": "2024-01-01 12:34:56,789",
"level": "INFO",
"message": "Processing request",
"context": {
"request": {
"id": "123e4567-e89b-12d3-a456-426614174000"
}
}
}
Context propagation
The context is automatically propagated to any jobs or messages dispatched during the request. This means that if you dispatch a job from your controller, the context will be available in that job as well:
import logging
from dataclasses import dataclass
from expanse.storage.asynchronous.storage_manager import StorageManager
logger = logging.getLogger(__name__)
@dataclass
class ProcessFileUpload:
file_path: str
async def handle(self, storage: StorageManager) -> None:
file = await storage.get(self.file_path)
# Process the file
logger.info("Processing file", extra={"file_path": self.file_path})
...
In the above example, the resulting log message in the job will also include the request.id value from the context:
12:34:57 INFO Processing file context file_path: files/uploaded_file.txt context: {"request": {"id": "123e4567-e89b-12d3-a456-426614174000"}}