Skip to main content

Management and injection of contextual variables into log messages.

Project description

Logctx

CICD PyPI - Status PyPI - Version PyPI - Python Version

PyPI - License PyPI - Types PyPI - Downloads

Logctx is a lightweight Python library that enhances logging with contextual information — useful for debugging, tracing, and observability. It integrates seamlessly with Python's built-in logging module and supports context management via decorators or context managers.

🚀 Features

  • Automatic Context Injection: Seamlessly inject context into log messages using Python's logging framework.
  • Scoped Contexts: Define context with decorators or context managers, supporting nested and inherited contexts.
  • Argument Logging: Automatically log function arguments as part of the context.
  • Runtime Context Manipulation: Update or clear contexts dynamically during runtime.
  • Thread and Async Safety: Supports threading and async.

📦 Installation

pip install logctx

Or with uv:

uv add logctx

⚡ Quick Start

Minimal usage example to get started. More information down below.

import logging

import logctx

# Setup log injection
root_logger = logging.getLogger()
console_handler = logging.StreamHandler()
formatter = logging.Formatter('%(message)s %(logctx)s')
context_filter = ContextInjectingLoggingFilter(output_field='logctx')
console_handler.setFormatter(formatter)
console_handler.addFilter(context_filter)
root_logger.addHandler(console_handler)
root_logger.setLevel(logging.DEBUG)

# Usage example
with new_context(user='alice'):
    root_logger.info('User logged in') # > User logged in {'user': 'alice'}

🧠 How It Works

Contexts are key-value pairs bound to a specific scope using context managers or decorators. These contexts are automatically included in log messages, providing additional information alongside the normal log content.

Contexts can also be nested, meaning that inner contexts inherit and may override key-value pairs from outer contexts.

🛠️ Context Managers

Use logctx.new_context() to create a new context:

import logctx

with logctx.new_context(user="alice", request_id="abc123"):
    # Each log message inside this context manager
    # now additionally carries the user and request_id.
    pass

You can inspect the current context using:

print(logctx.get_current().to_dict())

The root context, i.e. the context outside any active contextmanager or decorator, is protected to avoid accidental changes (since it would apply everywhere inside the current thread). You must access it via logctx.root. It provides the same methods as in logctx, without the ability to create a nested context via logctx.new_context().

🎯 Decorators

The decorators described here will always create a new basic context with the same scope as the decorated function/method. There is currently no support for using these decorators to manipulate the root context.

Static Context

Use @logctx.decorators.inject_context() to add static context to your functions.

Supports (sync & async): functions, generators, classmethods, instancemethods

import logctx

@logctx.decorators.inject_context(fn="my_function")
def my_function():
    print(logctx.get_current().to_dict())  # {'fn': 'my_function'}

Special Case: Generators

import logctx

@logctx.decorators.inject_context(inside=True)
def my_generator():
    # The context inside the generator execution will inherit 
    # the context that was active during initialization of 
    # the generator ({'inside': False, 'foo': 'bar'}),
    # BUT uses its own contexts (from the decorator or created inside) 
    # as child contexts. Hence the overridden inside attribute.
    print(logctx.get_current().to_dict()) # {'inside': True, 'foo': 'bar'}
    yield

with logctx.new_context(inside=False, foo="bar"):
    gen = my_generator()
    for _ in gen():
        print(logctx.get_current().to_dict()) # {'inside': False, 'foo': 'bar'}

Argument Logging

Automatically log function arguments as part of the context:

Supports: functions, classmethods, instancemethods

Note: Does not work properly with generator or async functions.

import logctx

# -> log arguments a and b as is, rename c to d
@logctx.decorators.log_arguments(args=["a", "b"], c="d")
def my_function(a, b, c):
    print(logctx.get_current().to_dict())  # {'a': 1, 'b': 2, 'd': 3}

my_function(1, 2, 3)

If one of the specified arguments is not found in the function signature during initialization, a ValueError will be raised. This also implicates that an extraction from *args or **kwargs is not possible.

🔄 Manipulating Contexts

Please note that the context attributes should never be updated directly through instances of LogContext (the return values of e.g. the contextmanagers and logctx.get_current()).

Below context manipulation functions work for contexts created by contextmanagers as well as context created by decorators. Use these to alter your current active context. In addition, they also work on logctx.root.


Updating Contexts

You can add or alter attributes of the current context by calling logctx.update():

import logctx

with logctx.new_context(section="main"):
    logctx.update(request_id="network_request_id")
    print(logctx.get_current().to_dict())  # {'section': 'main', 'request_id': 'network_request_id'}

Warning: Calling this method without an active context will change your root context, which may cause unexpected side effects. To reset your root context, use logctx.clear without an active context present.


Clearing Contexts

Clear the current context with logctx.clear():

import logctx

with logctx.new_context(section="main"):
    logctx.clear()
    print(logctx.get_current().to_dict())  # {}

Clearing a context will affect all key-value pairs of all contexts in scope, but the clearance will only last for the current active scope. After leaving the current scope, the context gets reset to its state before the leaving scope, essentially restoring some of its variables:

import logctx

with logctx.new_context(section="main"):
    with logctx.new_context(user="alice"):
        logctx.get_current().to_dict() # {'section': 'main', 'user': 'alice'}
        logctx.clear()
        logctx.get_current().to_dict() # {}
    
    logctx.get_current().to_dict() # {'section': 'main'}

🔗 Context Propagation

Generally, the contexts are isolated across threads / async on purpose. If you still want to propagate the context you can use the ContextPropagator object:

import logctx

propagator = logctx.ContextPropagator()
with logctx.new_context(event_id="1234"):
    propagator.capture()

with logctx.new_context():
    logctx.get_current() # > {}
    propagator.restore()
    logctx.get_current() # > {'event_id': #1234'}

🧩 Log Injection

Context gets injected into log messages with filter objects from Python's built-in logging module.

Configure your logger to include the context:

import logging

from pythonjsonlogger import jsonlogger
import logctx

# Configure logging
logger = logging.getLogger()
handler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter("%(message)s %(logctx)s")
handler.setFormatter(formatter)
handler.addFilter(logctx.ContextInjectingLoggingFilter(output_field="logctx"))
# output_field may be ommitted, which injects all context attributes at the root level of the log records.

logger.addHandler(handler)
logger.setLevel(logging.INFO)

# Inject context
with logctx.new_context(user="alice", request_id="abc123"):
    logger.info("User logged in")

# Output:
# {"message": "User logged in", "logctx": {"user": "alice", "request_id": "abc123"}}

Note: This example uses python-json-logger for better log formats.

🤝 Contributing

Contributions and feedback are welcome! Please reach out to me through issues or else before opening a pull request.

Thanks.

📚 See Also

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

logctx-1.0.0.tar.gz (37.8 kB view details)

Uploaded Source

Built Distribution

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

logctx-1.0.0-py3-none-any.whl (10.5 kB view details)

Uploaded Python 3

File details

Details for the file logctx-1.0.0.tar.gz.

File metadata

  • Download URL: logctx-1.0.0.tar.gz
  • Upload date:
  • Size: 37.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for logctx-1.0.0.tar.gz
Algorithm Hash digest
SHA256 045d6e123ea7be50b3b634bf88deef22ecd5254c4ad5af4aef3cefea448022dd
MD5 97891c160af464c65793c8bae357128a
BLAKE2b-256 1807ff24b88986a77ed9a1a14a1f31996c4471f19bdbd1796577360b3d00c0bd

See more details on using hashes here.

Provenance

The following attestation bundles were made for logctx-1.0.0.tar.gz:

Publisher: release.yml on aschulte201/logctx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file logctx-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: logctx-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 10.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for logctx-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bb3f4174520caea9c4d00232cfaf6ff35c8c9a332f833397c5a63d3de843a975
MD5 5682eb681eb986e78a1c4b5840e73a91
BLAKE2b-256 4e8e83e45a23d06aca7923cd71acdec7a592229db4a4d2074f0102f8b89a38b8

See more details on using hashes here.

Provenance

The following attestation bundles were made for logctx-1.0.0-py3-none-any.whl:

Publisher: release.yml on aschulte201/logctx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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