Skip to main content

A simple library for working with the context of logs.

Project description

unilogging

A simple library for working with the context of logs.

codecov PyPI version PyPI - Python Version PyPI - Downloads GitHub License GitHub Repo stars Telegram

Quickstart

pip install unilogging

What Problem Does It Solve?

Many modern Python applications use Dependency Injection (DI), but logging has traditionally relied on using a global logger (e.g., logging.getLogger(__name__)). While this simplifies the use of logging, it also introduces a number of complications.

If you're developing a web application, there are situations where you need to understand what happened during a specific request (for example, when an error occurs during that request). To do this, it's common to introduce a concept like request_id—a unique identifier assigned to an HTTP request, which allows you to trace all logs related to that request.

This creates a problem: you now need a way to propagate the request_id throughout your application – especially if your application has multiple architectural layers (e.g., infrastructure, service, presentation). Passing request_id (and other context data like user_id) explicitly everywhere is problematic, as it leads to a lot of boilerplate and argument duplication in your code, all just to enable proper logging.

How does Unilogging solve this?

Unilogging helps structure the logging process using Dependency Injection (DI).

For each request to your web application, a separate context is created. You can populate this context with data regardless of the application layer – using DI is all you need.

For example, consider this scenario:

  • You generate a request_id in a FastAPI middleware (or any other framework you’re using).
  • You write a log entry indicating that a certain action was performed, including relevant data (depending on your use case).

Here’s an example using Unilogging:

@app.middleware("http")
async def request_id_middleware(request, call_next):
    logger = await request.state.dishka_container.get(Logger)
    with logger.begin_scope(request_id=uuid.uuid4()):
        response = await call_next(request)
        return response

@app.post("/order")
@inject
async def create_order(
        data: CreateOrderAPIRequest,
        interactor: FromDishka[CreateOrderInteractor]
):
    order_id = await interactor(data)
    return order_id

class CreateOrderInteractor:
    def __init__(self, logger: Logger['CreateOrderInteractor']):
        self.logger = logger
    
    async def __call__(self, data: CreateOrderAPIRequest):
        order = ...
        self.logger.info("Created order", order_id=order.id)
{"message": "Created order", "logger_name": "module.path.CreateOrderInteractor", "request_id": "15c71f84-d0ed-49a6-a36e-ea179f0f62ef", "order_id": "15c71f84-d0ed-49a6-a36e-ea179f0f62ef"}

You don’t need to pass all this data between the layers of your application – you simply store it in a context that’s accessible throughout the entire application wherever DI is available.

How do other libraries solve this?

Here we’ll look at how other libraries propose to solve this problem.

logging (standard library)

Does not support context, and the only way to pass additional data to logs is:

logging.info("user logged in", extra={"user_id": user_id})

The problem here is that you have to pass all the required data explicitly – meaning that every function that logs something must accept all necessary data as arguments.

loguru

This is addressed by creating a new logger object that contains the context:

from loguru import logger

logger_with_request_id = logger.bind(request_id=str(uuid.uuid4))
logger_with_request_and_user_id = logger_with_request_id.bind(user_id=user.id)

However, you can’t use this in a Dependency Injection approach – it recreates the logger and doesn’t function as a true logging context in the full sense.

structlog

This library handles context using contextvars, but there are significant issues with this approach. Implementing context via contextvars is not entirely safe, as it relies on thread-local storage.

  • Context can leak into other requests if you forget to clear all the context variables used during a request.
  • You can lose the request context if you launch an operation in a separate thread within that request.

For synchronous applications, this may be acceptable, but you still need to manually clear the entire context at the start of each new request. In asynchronous applications, the context must remain within the boundaries of await, since coroutines execute within a single thread.

Working with context looks like this:

log = structlog.get_logger()

clear_contextvars()
bind_contextvars(a=1, b=2)
log.info("hello")
event='hello' a=1 b=2

Features

Logging Contexts and Integration with Dishka

One of the main features of Unilogging is the ability to conveniently pass values into a context, the data from which can later be used by your formatter. This is similar to the extra argument in Python's standard logging.

Unilogging offers new possibilities with a more convenient API. You can populate the context with data at various stages of your application's execution, and logger classes below will pick up this context at any level of the application. This works within the REQUEST-scope.

Here’s an example to illustrate – a middleware in a FastAPI application that generates a request_id and adds it to the context.

@app.middleware("http")
async def request_id_middleware(request, call_next):
    logger = await request.state.dishka_container.get(Logger)
    with logger.begin_scope(request_id=uuid.uuid4()):
        response = await call_next(request)
        return response

Generic logger name or your own factory (Integration with Dishka)

You can retrieve a logger from the DI container as follows:

class SomeClass:
    def __init__(self, logger: Logger['SomeClass']):
        ...

In this case, when using the standard integration with Dishka, a new logger will be created with the name your_module.path_to_class.SomeClass. If you don’t need this, you can avoid using a generic logger – in that case, the logger name will be unilogging.Logger, or you can pass your own factory into the integration.

The default logger factory in the provider is used so that you can supply your own factory with custom logic for creating standard loggers – for example, if you want logger names to be generated based on different criteria. However, your factory must conform to the StdLoggerFactory protocol.

Your factory should follow the protocol below:

class StdLoggerFactory(Protocol):
    def __call__(self, generic_type: type, default_name: str = ...) -> logging.Logger:
        ...

Then you can pass it like this:

UniloggingProvider(std_logger_factory=your_factory)

Templating – Injecting values from the context

You can use the built-in log record formatting provided by the library. At the stage of passing the record to the standard logger, it formats the message using format_map, injecting the entire current context. This feature is typically used when your logs are output in JSON format.

with logger.begin_context(user_id=user.id):
    logger.info("User {user_id} logged in using {auth_method} auth method", auth_method="telegram")
INFO:unilogging.Logger:User 15c71f84-d0ed-49a6-a36e-ea179f0f62ef logged in using telegram auth method

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

unilogging-0.2.1.tar.gz (31.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

unilogging-0.2.1-py3-none-any.whl (8.1 kB view details)

Uploaded Python 3

File details

Details for the file unilogging-0.2.1.tar.gz.

File metadata

  • Download URL: unilogging-0.2.1.tar.gz
  • Upload date:
  • Size: 31.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for unilogging-0.2.1.tar.gz
Algorithm Hash digest
SHA256 a2c6a7541677a7e8b58747dfc6bde4c1838812872dd11476087ea8eca1569a4c
MD5 6ceac9ea570722f45c7d6b01a75f12b5
BLAKE2b-256 077f818b21623b1607b61d1b0941726834a765b595353b714f3599ac7ae148b4

See more details on using hashes here.

File details

Details for the file unilogging-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: unilogging-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 8.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for unilogging-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b6062fda9b4a98b49fd184981e5866282f281d64d07bf6c50e7b5bcf9f7eb39c
MD5 741ecfdc4122b8aa6b8da2f1ad39e1cc
BLAKE2b-256 89c3b3214699328a28f081867e2ebc2c0278a856b90d03ee1772719341c7f680

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page