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.

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.1.1.tar.gz (7.7 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.1.1-py3-none-any.whl (7.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: unilogging-0.1.1.tar.gz
  • Upload date:
  • Size: 7.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.23

File hashes

Hashes for unilogging-0.1.1.tar.gz
Algorithm Hash digest
SHA256 285ee332b7a515711392d4d3571775613678a74925ca2762a5a5db4d490398c7
MD5 e674f72ffd59fe4ad305802714984afa
BLAKE2b-256 4b6e7c46064388368224467994ec9fc43bd4e7132646fd997e26284ff45a1561

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for unilogging-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 36f427bbf7f1db610abbf0c7bfb69788cd3a04fa65a2cccc1e18393aa59c28d2
MD5 2f8880cf6dcf109a65cb7cd94bebbbd6
BLAKE2b-256 462983ede282ff42baeffd3e40d1604def30c4a4fe77b6537a973e59b4c666b6

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