Skip to main content

Framework-agnostic AOP-style logging decorators with optional loguru support

Project description

logcraft

中文文档

A lightweight AOP-style logging toolkit for Python with framework-agnostic design.

It helps remove manual logger.info(...) boilerplate by injecting structured logs through decorators and context managers.

Key Features:

  • 🎯 Framework-agnostic: Works with Python's standard logging module by default, with optional loguru support
  • 🔍 Conditional logging: Filter decorator support for fine-grained logging control
  • 📝 Fully documented: Comprehensive docstrings on all public APIs
  • 🛠️ Code quality: Built-in linting, formatting, and testing tools

Install

pip install logcraft-aop

Optional backends:

# For loguru backend (optional)
pip install logcraft-aop[loguru]

Quick start

from logcraft import get_logger, log_calls, log_class, no_log, log_context, setup_logging

# Initialize with stdlib backend (default)
setup_logging(level="INFO", enable_file=False)
logger = get_logger(__name__)


@log_calls
def add(a: int, b: int) -> int:
    return a + b


# Conditional logging with filter
@log_calls(filter=lambda x, y: x > 0)
def conditional_add(x: int, y: int) -> int:
    return x + y  # Only logs when x > 0


@log_class(default=True, exclude=["helper"])
class Service:
    def run(self, x: int) -> int:
        return x * 2

    @no_log
    def helper(self) -> str:
        return "skip"


with log_context("batch.process", size=10) as ctx:
    ctx.bind(done=10)

Backend Selection

Logcraft supports multiple logging backends. Python's standard logging module is used by default, making it framework-agnostic.

Using stdlib backend (default)

from logcraft import setup_logging

# Default is stdlib backend
setup_logging(level="INFO", log_dir="logs")

Using loguru backend

from logcraft import setup_logging

# Specify loguru backend in setup_logging
setup_logging(backend="loguru", level="INFO", log_dir="logs")

API Reference

setup_logging()

Initialize the logging system. Call once at program startup.

from logcraft import setup_logging

setup_logging(
    level="INFO",        # or set via LOG_LEVEL env var
    log_dir="logs",      # or set via LOGCRAFT_LOG_DIR env var
    backend="stdlib",    # "stdlib" (default) or "loguru"
    **options            # Backend-specific options
)

If level is omitted, it falls back to the LOG_LEVEL environment variable, then "INFO". If log_dir is omitted, it falls back to LOGCRAFT_LOG_DIR, then "logs".

Backend-specific options:

For stdlib backend:

  • format: Custom log format string
  • datefmt: Date format string
  • enable_console: Enable console output (default: True)
  • enable_file: Enable file output (default: True)

For loguru backend:

  • enable_console: Enable console output (default: True)
  • enable_file: Enable file output (default: True)
  • rotation: Log rotation setting (default: "1 day")
  • retention: Number of rotated files to keep (default: 3)

The log format is structured and includes timestamp, level, PID, thread ID, module, and line number:

2026-05-15 10:30:00.123 || INFO || 12345 || 67890 || myapp:42 || add.done || a=1, b=2, result=3

get_logger(name)

Return a LoggerProtocol instance bound to the given module name.

logger = get_logger(__name__)
logger.info("user.login", user_id=42)

LoggerProtocol

The protocol that all logger backends implement. This replaces the previous Logger class.

Method Description
debug(event, **fields) Emit at DEBUG level
info(event, **fields) Emit at INFO level
warning(event, **fields) Emit at WARNING level
error(event, **fields) Emit at ERROR level
exception(event, **fields) Emit at ERROR level with traceback attached
bind(**ctx) Return a new Logger with persistent context fields
context(event, **fields) Return a LogContext manager (see below)

bind() is useful for attaching correlation fields that propagate across calls:

req_log = logger.bind(request_id="abc-123")
req_log.info("handler.start")
req_log.info("handler.end", status=200)
# both lines include request_id=abc-123

@log_calls

Decorator that automatically logs function entry (.done) and exceptions (.error).

@log_calls
def add(a: int, b: int) -> int:
    return a + b

Calling add(1, 2) emits:

add.done || a=1, b=2, result=3

If the function raises, it emits .error and re-raises the exception:

@log_calls
def fail():
    raise ValueError("bad")

Calling fail() emits:

fail.error || error=ValueError: bad

Parameters:

Parameter Default Description
include_result True Attach the return value as result= in the .done log
message None Custom event name prefix (default is fn.__qualname__)
level "info" Log level for .done events (.error always uses error)
filter None Callable that receives function args and returns bool to decide whether to log

Conditional logging with filter:

The filter parameter intelligently passes only the parameters it accepts. If filter has **kwargs, all parameters are passed. Otherwise, only explicitly named parameters are passed. Special parameter func_name is available if filter has it.

1. Filter accepts specific parameters only:

# Only uses x parameter - y is ignored
@log_calls(filter=lambda x: x > 0)
def process(x: int, y: int) -> int:
    return x + y

# Only uses user_id - action is ignored
@log_calls(filter=lambda user_id: user_id.startswith("admin_"))
def admin_action(user_id: str, action: str) -> None:
    pass

2. Filter accepts func_name for name-based filtering:

# Only uses func_name - all other params ignored
@log_calls(filter=lambda func_name: "important" in func_name)
def important_operation(data: dict) -> None:
    pass

# Combines func_name with other params
@log_calls(filter=lambda user_id, func_name: "payment" in func_name and user_id.startswith("admin_"))
def process_payment(user_id: str, amount: float) -> str:
    return "tx_123"

**3. Filter accepts kwargs (receives everything):

@log_calls(filter=lambda **kwargs: kwargs.get("amount", 0) > 1000)
def process_payment(user_id: str, amount: float) -> str:
    return "tx_123"

Custom event name:

@log_calls(message="payment.charge", include_result=False)
def charge(user_id: str, amount: float) -> str:
    return "tx_123"

Emits payment.charge.done || user_id=alice, amount=9.99 (no result=).

Async support: @log_calls detects coroutines automatically and wraps them with async def.

@log_calls
async def fetch(url: str) -> str:
    ...

@log_class

Class decorator that applies @log_calls to selected methods. It also injects a self._log attribute (a Logger instance) if the class doesn't already define one.

@log_class(default=True, exclude=["helper"])
class Service:
    def run(self, x: int) -> int:
        return x * 2

    def helper(self) -> str:
        return "skip"

run gets logged as Service.run.done, while helper is excluded.

Parameters:

Parameter Default Description
default True If True, log all methods (minus exclude). If False, only log methods in include.
include None List of method names to log (used when default=False)
exclude None List of method names to skip (used when default=True)
skip_private True Automatically skip methods starting with _ (dunder methods are always skipped)
include_result False Whether to include return values in .done logs
level "info" Log level for .done events
filter None Callable that receives method args and returns bool to decide whether to log

Conditional logging with filter:

The filter parameter intelligently passes only the parameters it accepts. If filter has **kwargs, all parameters are passed. Otherwise, only explicitly named parameters are passed. Special parameter func_name is available if filter has it.

# Only log methods with "process" in the name
@log_class(filter=lambda func_name: "process" in func_name)
class Processor:
    def process(self, value: int) -> int:
        return value * 2

    def validate(self, value: int) -> bool:
        return value > 0  # Not logged (no "process" in name)

# Filter by value only - self and func_name ignored
@log_class(filter=lambda value: value > 100)
class Processor2:
    def process(self, value: int) -> int:
        return value * 2

Opt-in mode (default=False):

@log_class(default=False, include=["run"])
class Service:
    def run(self, x: int) -> int:
        return x * 2

    def helper(self) -> str:
        return "skip"

Only run is logged.

Custom logger on the instance:

If __init__ sets self._log, @log_class preserves it and uses it for all wrapped methods:

@log_class(default=True, include=["run"])
class Manual:
    def __init__(self):
        self._log = get_logger("manual").bind(tag="x")

    def run(self) -> str:
        return "ok"

Emits Manual.run.done || tag=x.

@no_log

Mark a method to be skipped by @log_class, even if it would otherwise be included.

@log_class(default=True)
class Worker:
    def run(self): ...       # logged
    def process(self): ...   # logged

    @no_log
    def internal(self): ...  # skipped

log_context()

Context manager for block-level logging. Emits .done on normal exit, .error on exception.

with log_context("batch.process", size=100) as ctx:
    ctx.bind(processed=42)

Normal exit emits:

batch.process.done || size=100, processed=42

If an exception occurs:

with log_context("batch.process", size=100):
    raise RuntimeError("disk full")

Emits:

batch.process.error || size=100, error=RuntimeError: disk full

Parameters:

Parameter Default Description
event required Event name prefix
logger None Custom Logger instance (default: uses the global logcraft logger)
level "info" Log level for .done events
**fields Static fields included in both .done and .error

ctx.bind(**fields) adds fields that accumulate during the block and are emitted at exit.

Async support: log_context works with async with:

async with logger.context("api.call", endpoint="/users") as ctx:
    ctx.bind(status=200)

LogContext

The object returned by log_context() and logger.context(). You typically don't construct it directly.

Method Description
bind(**fields) Accumulate fields to include in the final log
__enter__ / __exit__ Synchronous context manager protocol
__aenter__ / __aexit__ Async context manager protocol

Configuration

from logcraft import setup_logging

setup_logging(
    level="INFO",
    log_dir="logs",
    when="D",
    interval=1,
    backup_count=3,
    enable_file=True,
)

Environment variables

Variable Default Description
LOG_LEVEL "INFO" Default log level when level param is not passed
LOGCRAFT_LOG_DIR "logs" Default log directory when log_dir param is not passed

Development shortcuts

make help
make install
make test
make build
make clean
make lint           # run ruff linting and formatting
make publish-test   # publish to TestPyPI
make publish        # publish to PyPI

How it works

  • Decorators capture function parameters via inspect.signature(...)
  • self / cls parameters are automatically stripped from logged fields
  • Success path emits <event>.done
  • Error path emits <event>.error and re-raises exception
  • @log_class only wraps methods defined in cls.__dict__ (not inherited ones) to avoid side effects
  • @log_class injects self._log on first call if the instance doesn't already have one
  • @no_log sets fn._no_log = True which @log_class checks before wrapping

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

logcraft_aop-2026.5.18.tar.gz (17.0 kB view details)

Uploaded Source

Built Distribution

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

logcraft_aop-2026.5.18-py3-none-any.whl (16.6 kB view details)

Uploaded Python 3

File details

Details for the file logcraft_aop-2026.5.18.tar.gz.

File metadata

  • Download URL: logcraft_aop-2026.5.18.tar.gz
  • Upload date:
  • Size: 17.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.0 CPython/3.11.15 Linux/6.17.0-1013-azure

File hashes

Hashes for logcraft_aop-2026.5.18.tar.gz
Algorithm Hash digest
SHA256 58f8f9f8f2490edb966622391e14594f448cf61f1120199d6d736500bf8a6f20
MD5 378f2d691390d32fce96533857a9ee48
BLAKE2b-256 7b94f4f1881158b1bd5810db765cbbf022314ce5c7417f439de708c3e7b991f5

See more details on using hashes here.

File details

Details for the file logcraft_aop-2026.5.18-py3-none-any.whl.

File metadata

  • Download URL: logcraft_aop-2026.5.18-py3-none-any.whl
  • Upload date:
  • Size: 16.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.0 CPython/3.11.15 Linux/6.17.0-1013-azure

File hashes

Hashes for logcraft_aop-2026.5.18-py3-none-any.whl
Algorithm Hash digest
SHA256 c151df2b462db1c76d233b05059dca54af56cd0cf8a16cd223126bea3f9c9b7b
MD5 3b4340f68e005676f7a642f28e788279
BLAKE2b-256 3653f41ef7316cf83b1aba6103bc5fea1891317b5e7f47cc818ed4dfd000692e

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