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"}}