unilogging


Nameunilogging JSON
Version 0.2.2 PyPI version JSON
download
home_pageNone
SummaryA simple library for working with the context of logs.
upload_time2025-10-17 22:15:33
maintainerNone
docs_urlNone
authorNone
requires_python>=3.12
licenseMIT
keywords context logging python-logging structured-logging
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # unilogging

A simple library for working with the context of logs.

[![codecov](https://codecov.io/gh/goduni/unilogging/branch/master/graph/badge.svg)](https://codecov.io/gh/goduni/unilogging)
[![PyPI version](https://img.shields.io/pypi/v/unilogging.svg)](https://pypi.org/project/unilogging)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/unilogging)
![PyPI - Downloads](https://img.shields.io/pypi/dm/unilogging)
![GitHub License](https://img.shields.io/github/license/goduni/unilogging)
![GitHub Repo stars](https://img.shields.io/github/stars/goduni/unilogging)
[![Telegram](https://img.shields.io/badge/💬-Telegram-blue)](https://t.me/+TvprI2G1o7FmYzRi)

## Quickstart

```bash
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:

```python
@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)
```

```json
{"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:

```python
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:

```python
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:


```python
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.

```python
@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:

```python
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:

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

Then you can pass it like this:

```python
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.

```python
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
```

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "unilogging",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.12",
    "maintainer_email": null,
    "keywords": "context, logging, python-logging, structured-logging",
    "author": null,
    "author_email": "Universe Interface <uni@goduni.me>",
    "download_url": "https://files.pythonhosted.org/packages/ae/de/50e37db1b607a34ede8656878f7248580c403987e5deb8af1c383fa63525/unilogging-0.2.2.tar.gz",
    "platform": null,
    "description": "# unilogging\n\nA simple library for working with the context of logs.\n\n[![codecov](https://codecov.io/gh/goduni/unilogging/branch/master/graph/badge.svg)](https://codecov.io/gh/goduni/unilogging)\n[![PyPI version](https://img.shields.io/pypi/v/unilogging.svg)](https://pypi.org/project/unilogging)\n![PyPI - Python Version](https://img.shields.io/pypi/pyversions/unilogging)\n![PyPI - Downloads](https://img.shields.io/pypi/dm/unilogging)\n![GitHub License](https://img.shields.io/github/license/goduni/unilogging)\n![GitHub Repo stars](https://img.shields.io/github/stars/goduni/unilogging)\n[![Telegram](https://img.shields.io/badge/\ud83d\udcac-Telegram-blue)](https://t.me/+TvprI2G1o7FmYzRi)\n\n## Quickstart\n\n```bash\npip install unilogging\n```\n\n## What Problem Does It Solve?\n\nMany 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.\n\nIf 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`\u2014a unique identifier assigned to an HTTP request, which allows you to trace all logs related to that request.\n\nThis creates a problem: you now need a way to propagate the request_id throughout your application \u2013 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.\n\n\n\n### How does Unilogging solve this?\n\nUnilogging helps structure the logging process using Dependency Injection (DI).\n\nFor each request to your web application, a separate context is created. You can populate this context with data regardless of the application layer \u2013 using DI is all you need.\n\nFor example, consider this scenario:\n\n* You generate a `request_id` in a FastAPI middleware (or any other framework you\u2019re using).\n* You write a log entry indicating that a certain action was performed, including relevant data (depending on your use case).\n\nHere\u2019s an example using Unilogging:\n\n```python\n@app.middleware(\"http\")\nasync def request_id_middleware(request, call_next):\n    logger = await request.state.dishka_container.get(Logger)\n    with logger.begin_scope(request_id=uuid.uuid4()):\n        response = await call_next(request)\n        return response\n\n@app.post(\"/order\")\n@inject\nasync def create_order(\n        data: CreateOrderAPIRequest,\n        interactor: FromDishka[CreateOrderInteractor]\n):\n    order_id = await interactor(data)\n    return order_id\n\nclass CreateOrderInteractor:\n    def __init__(self, logger: Logger['CreateOrderInteractor']):\n        self.logger = logger\n    \n    async def __call__(self, data: CreateOrderAPIRequest):\n        order = ...\n        self.logger.info(\"Created order\", order_id=order.id)\n```\n\n```json\n{\"message\": \"Created order\", \"logger_name\": \"module.path.CreateOrderInteractor\", \"request_id\": \"15c71f84-d0ed-49a6-a36e-ea179f0f62ef\", \"order_id\": \"15c71f84-d0ed-49a6-a36e-ea179f0f62ef\"}\n```\n\nYou don\u2019t need to pass all this data between the layers of your application \u2013 you simply store it in a context that\u2019s accessible throughout the entire application wherever DI is available.\n\n\n\n### How do other libraries solve this?\n\nHere we\u2019ll look at how other libraries propose to solve this problem.\n\n\n\n### logging (standard library)\n\nDoes not support context, and the only way to pass additional data to logs is:\n\n```python\nlogging.info(\"user logged in\", extra={\"user_id\": user_id})\n```\n\nThe problem here is that you have to pass all the required data explicitly \u2013 meaning that every function that logs something must accept all necessary data as arguments.\n\n### loguru\n\nThis is addressed by creating a new logger object that contains the context:\n\n```python\nfrom loguru import logger\n\nlogger_with_request_id = logger.bind(request_id=str(uuid.uuid4))\nlogger_with_request_and_user_id = logger_with_request_id.bind(user_id=user.id)\n```\n\nHowever, you can\u2019t use this in a Dependency Injection approach \u2013 it recreates the logger and doesn\u2019t function as a true logging context in the full sense.\n\n\n\n### structlog\n\nThis library handles context using `contextvars`, but there are significant issues with this approach.\nImplementing context via `contextvars` is not entirely safe, as it relies on thread-local storage.\n\n* Context can leak into other requests if you forget to clear all the context variables used during a request.\n* You can lose the request context if you launch an operation in a separate thread within that request.\n\nFor 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.\n\nWorking with context looks like this:\n\n\n```python\nlog = structlog.get_logger()\n\nclear_contextvars()\nbind_contextvars(a=1, b=2)\nlog.info(\"hello\")\n```\n```\nevent='hello' a=1 b=2\n```\n\n\n\n## Features\n\n### Logging Contexts and Integration with Dishka\n\nOne 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.\n\nUnilogging 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. \n\nHere\u2019s an example to illustrate \u2013 a middleware in a FastAPI application that generates a request_id and adds it to the context.\n\n```python\n@app.middleware(\"http\")\nasync def request_id_middleware(request, call_next):\n    logger = await request.state.dishka_container.get(Logger)\n    with logger.begin_scope(request_id=uuid.uuid4()):\n        response = await call_next(request)\n        return response\n```\n\n\n\n### Generic logger name or your own factory (Integration with Dishka)\n\nYou can retrieve a logger from the DI container as follows:\n\n```python\nclass SomeClass:\n    def __init__(self, logger: Logger['SomeClass']):\n        ...\n```\n\nIn 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\u2019t need this, you can avoid using a generic logger \u2013 in that case, the logger name will be `unilogging.Logger`, or you can pass your own factory into the integration.\n\nThe default logger factory in the provider is used so that you can supply your own factory with custom logic for creating standard loggers \u2013 for example, if you want logger names to be generated based on different criteria. However, your factory must conform to the `StdLoggerFactory` protocol.\n\nYour factory should follow the protocol below:\n\n```python\nclass StdLoggerFactory(Protocol):\n    def __call__(self, generic_type: type, default_name: str = ...) -> logging.Logger:\n        ...\n```\n\nThen you can pass it like this:\n\n```python\nUniloggingProvider(std_logger_factory=your_factory)\n```\n\n\n\n### Templating \u2013 Injecting values from the context\n\nYou 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.\n\n```python\nwith logger.begin_context(user_id=user.id):\n    logger.info(\"User {user_id} logged in using {auth_method} auth method\", auth_method=\"telegram\")\n```\n```\nINFO:unilogging.Logger:User 15c71f84-d0ed-49a6-a36e-ea179f0f62ef logged in using telegram auth method\n```\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "A simple library for working with the context of logs.",
    "version": "0.2.2",
    "project_urls": {
        "Homepage": "https://github.com/goduni/unilogging",
        "Issues": "https://github.com/goduni/unilogging/issues",
        "Repository": "https://github.com/goduni/unilogging"
    },
    "split_keywords": [
        "context",
        " logging",
        " python-logging",
        " structured-logging"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "051dc41f053fddaa3aacb46cd4e6abc8e0d1f252f307d13b0d3cc896af9b4efd",
                "md5": "d32d2ac0c5846c28cfd811f47454754e",
                "sha256": "9c2ceccfae701a7139d3e9f843949a0f000c31c2ffc900163c5f001da6a99d38"
            },
            "downloads": -1,
            "filename": "unilogging-0.2.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "d32d2ac0c5846c28cfd811f47454754e",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.12",
            "size": 8135,
            "upload_time": "2025-10-17T22:15:32",
            "upload_time_iso_8601": "2025-10-17T22:15:32.153979Z",
            "url": "https://files.pythonhosted.org/packages/05/1d/c41f053fddaa3aacb46cd4e6abc8e0d1f252f307d13b0d3cc896af9b4efd/unilogging-0.2.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "aede50e37db1b607a34ede8656878f7248580c403987e5deb8af1c383fa63525",
                "md5": "c01ae8789b1929ddabc49aab61155a31",
                "sha256": "3273a3458c43706f63773972a830045baeaaaec84e97c0d11d45fc70ccac8174"
            },
            "downloads": -1,
            "filename": "unilogging-0.2.2.tar.gz",
            "has_sig": false,
            "md5_digest": "c01ae8789b1929ddabc49aab61155a31",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.12",
            "size": 31098,
            "upload_time": "2025-10-17T22:15:33",
            "upload_time_iso_8601": "2025-10-17T22:15:33.140400Z",
            "url": "https://files.pythonhosted.org/packages/ae/de/50e37db1b607a34ede8656878f7248580c403987e5deb8af1c383fa63525/unilogging-0.2.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-10-17 22:15:33",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "goduni",
    "github_project": "unilogging",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "unilogging"
}
        
Elapsed time: 2.08979s