Skip to main content

No more logging in your code business logic with decorators

Project description

GitHub latest version Tests Publish Size License

logveil

No more logging in your code business logic with python decorators.

Logging is a very powerful tool for debugging and monitoring your code. But if you are often adding logging statements, you will quickly find your code overcrowded with them.

Fortunately, you can avoid this by using python decorators. This library provides easy logging for your code without stealing readability and maintainability. It also provides stack traces with full local variables, value sanitization, and async support.

Requires Python 3.9+

Installation

pip install logveil

How to use

Add the decorator to your function. logveil will automatically write log messages to Python's standard logging system — they appear in your terminal/console output, not in your source code.

import logging
from logveil import log_on_start, log_on_end, log_on_error

logging.basicConfig(level=logging.DEBUG)


@log_on_start
@log_on_end
def add(a, b):
    return a + b


@log_on_error
def divide(a, b):
    return a / b


if __name__ == '__main__':
    add(1, b=2)
    divide("abc", "def")

Running this script produces the following output in your terminal:

INFO:__main__:Start func add with args (1,), kwargs {'b': 2}
INFO:__main__:Successfully run func add with args (1,), kwargs {'b': 2}
ERROR:__main__:Error in func divide with args ('abc', 'def'), kwargs {}
	unsupported operand type(s) for /: 'str' and 'str'.

Async support

All decorators work with async functions automatically — no extra configuration needed:

import asyncio
import logging
from logveil import log_on_start, log_on_end, log_on_error

logging.basicConfig(level=logging.DEBUG)


@log_on_start
@log_on_end
@log_on_error(reraise=False)
async def fetch_data(url, timeout=30):
    await asyncio.sleep(0.01)  # simulate async I/O
    return {"status": "ok"}


asyncio.run(fetch_data("https://api.example.com/data", timeout=10))
INFO:__main__:Start func fetch_data with args ('https://api.example.com/data',), kwargs {'timeout': 10}
INFO:__main__:Successfully run func fetch_data with args ('https://api.example.com/data',), kwargs {'timeout': 10}

Sensitive and sanitize_params work the same way with async functions:

@log_on_start
async def connect(host, token):
    ...

asyncio.run(connect("example.com", Sensitive("sk-secret-token")))
# INFO:__main__:Start func connect with args ("'example.com'", '***'), kwargs {}

Value sanitization

Prevent sensitive values from appearing in logs using Sensitive or sanitize_params.

Imagine you have an API client that passes a token into request headers:

import logging
import requests
from logveil import log_on_error, Sensitive

logging.basicConfig(level=logging.DEBUG)


def call_api(url, headers):
    response = requests.get(url, headers=headers)
    response.raise_for_status()
    return response.json()


@log_on_error(trace_stack=True, reraise=False)
def get_user_profile(api_url, token):
    headers = {"Authorization": f"Bearer {token}"}
    return call_api(api_url, headers)

Without Sensitive, when the request crashes, your token is exposed in the log message and in the stack trace locals:

get_user_profile("https://api.example.com/user", "sk-1234-secret-token")
ERROR:__main__:Error in func get_user_profile with args
  ('https://api.example.com/user', 'sk-1234-secret-token'), kwargs {}
                                    ^^^^^^^^^^^^^^^^^^^^
                                    Token is visible!
Traceback (most recent call last):
  ...
  File "app.py", line 15, in get_user_profile
    return call_api(api_url, headers)
    token = 'sk-1234-secret-token'                 <-- visible in locals!
    headers = {'Authorization': 'Bearer sk-1234-secret-token'}
  ...
ConnectionError: ...

Wrap the value with Sensitive — the function receives the real value, but logveil redacts it from both the log message and stack trace locals:

get_user_profile("https://api.example.com/user", Sensitive("sk-1234-secret-token"))
ERROR:__main__:Error in func get_user_profile with args
  ('https://api.example.com/user', '***'), kwargs {}
                                    ^^^
                                    Redacted!
Traceback (most recent call last):
  ...
  File "app.py", line 15, in get_user_profile
    return call_api(api_url, headers)
    token = '***'                                  <-- redacted (parameter)
    headers = '***'                                <-- redacted (derived, contains token)
  ...
ConnectionError: ...

You can also auto-redact by parameter name with sanitize_params:

@log_on_start(sanitize_params={"token", "password", "secret"})
def connect(host, token):
    ...

connect("example.com", "sk-abc123")
# INFO:__main__:Start func connect with args ('example.com', '***'), kwargs {}

How redaction works in stack traces

logveil uses three-layer analysis to precisely redact sensitive values from stack trace locals while preserving useful debugging information:

  1. Name-based: The decorated function's parameter (e.g. token) is always redacted by name. Works for any type — strings, ints, dicts, etc.
  2. Bytecode dataflow: logveil analyzes the function's bytecode to find which other local variables were derived from the sensitive parameter.
  3. Value check: Among dataflow-tainted variables, only those whose runtime value actually contains the secret data are redacted. Transformations that discard the secret (like len()) are left visible.
@log_on_error(trace_stack=True)
def process(token):
    header = f"Bearer {token}"       # derived, contains the secret
    secrets = [token, "other"]       # container holding the object
    token_len = len(token)           # derived, but value (14) doesn't contain secret
    coincidence = "sk-1234-..."      # same value, but NOT derived from token
    raise ValueError("boom")
File "app.py", line 10, in process
    raise ValueError("boom")
    token = '***'                    <-- redacted (parameter name)
    header = '***'                   <-- redacted (in dataflow + contains secret)
    secrets = '***'                  <-- redacted (container holds the object)
    token_len = 14                   <-- preserved (in dataflow, but 14 ≠ secret)
    coincidence = 'sk-1234-...'      <-- preserved (not in dataflow from token)

Stack trace with local variables

When debugging, a plain traceback often isn't enough — you need to see what values caused the error. Use trace_stack=True to get a full stack trace with local variables captured at each frame:

import logging
from logveil import log_on_error

logging.basicConfig(level=logging.DEBUG)


def calculate_average(numbers):
    total = sum(numbers)
    count = len(numbers)
    return total / count


@log_on_error(trace_stack=True, reraise=False)
def process_data(data):
    filtered = [x for x in data if x is not None]
    result = calculate_average(filtered)
    return result


process_data([None, None, None])

Without trace_stack, you only see the call chain:

ERROR:__main__:Error in func process_data ...
Traceback (most recent call last):
  File "demo.py", line 16, in process_data
    result = calculate_average(filtered)
  File "demo.py", line 10, in calculate_average
    return total / count
ZeroDivisionError: division by zero.

With trace_stack=True, each frame shows its local variables — so you can immediately see why it failed (count = 0, filtered = []):

ERROR:__main__:Error in func process_data ...
Traceback (most recent call last):
  File "demo.py", line 16, in process_data
    result = calculate_average(filtered)
    data = [None, None, None]
    filtered = []
  File "demo.py", line 10, in calculate_average
    return total / count
    count = 0
    numbers = []
    total = 0
ZeroDivisionError: division by zero.

No need to add print statements or reproduce the bug — the log tells you everything.

What logveil can do for you

Decorators

logveil provides three decorators:

  • log_on_start: Log when the function is called.
  • log_on_end: Log when the function finishes successfully.
  • log_on_error: Log when the function raises an exception.

Use variables in messages

The message given to decorators is treated as a format string which takes the function arguments as format arguments.

import logging
from logveil import log_on_start

logging.basicConfig(level=logging.DEBUG)


@log_on_start(logging.INFO, 'Start func {callable.__name__} with args {args}, kwargs {kwargs}')
def hello(name):
    print("Hello, {}".format(name))


if __name__ == "__main__":
    hello("World")

Which will print: INFO:__main__:Start func hello with args ('World',), kwargs {}

Documentation

Format variables

The following variables are available in the format string:

Default variable name Description LogOnStart LogOnEnd LogOnError
callable The function object Yes Yes Yes
args/kwargs The arguments, key arguments passed to the function Yes Yes Yes
result The return value of the function No Yes No
error The error object if the function is finished with error No No Yes

Base arguments

Available arguments in all decorators:

Argument Description
logger The logger object. If no logger is given, logveil will create one with the module name where the function is defined. Default is logging.getLogger(callable.__module__)
handler A custom log handler object. Only available if no logger object is given.
args_kwargs Set True to use {args}, {kwargs} format, or False to use function parameter names. Default True
callable_format_variable The format variable name for the callable. Default is callable
trace_stack Set to True to get the full stack trace. Default is False
capture_locals Set to True to capture local variables in stack frames. Default is False (or trace_stack on log_on_error)
include_decorator Set to True to include logveil frames in the stack trace. Default is False
sanitize_params A set of parameter names to auto-redact in log messages. Default is None

log_on_start

Argument Description
level The level of the log message. Default is logging.INFO
message The message to log. Can use {args} {kwargs} or function parameter names, but not both. Default is Start func {callable.__name__} with args {args}, kwargs {kwargs}

log_on_end

Argument Description
level The level of the log message. Default is logging.INFO
message The message to log. Can use {args} {kwargs} or function parameter names, but not both. Default is Successfully run func {callable.__name__} with args {args}, kwargs {kwargs}
result_format_variable The format variable name for the return value. Default is result

log_on_error

Argument Description
level The level of the log message. Default is logging.ERROR
message The message to log. Can use {args} {kwargs} or function parameter names, but not both. Default is Error in func {callable.__name__} with args {args}, kwargs {kwargs}\n{error}
on_exceptions Exception classes to catch and log. Default catches all exceptions.
reraise Whether to reraise the exception after logging. Default is True
exception_format_variable The format variable name for the exception. Default is error

Extras

Custom exception hook

Override the default exception hook to write crash logs with local variable capture:

import logveil

logveil.system_excepthook_overwrite()  # Overwrite the default exception hook

This replaces sys.excepthook with logveil's handler, which writes detailed crash information to a file.

Argument Description
out_file The path to the file to write the crash log. Default is crash.log

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

logveil-2.2.0.tar.gz (27.5 kB view details)

Uploaded Source

Built Distribution

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

logveil-2.2.0-py3-none-any.whl (16.0 kB view details)

Uploaded Python 3

File details

Details for the file logveil-2.2.0.tar.gz.

File metadata

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

File hashes

Hashes for logveil-2.2.0.tar.gz
Algorithm Hash digest
SHA256 a9a31c6c2af1057cdbd1eb3b4c01c68d86a8928bc573cbc085e0ee119c38e87e
MD5 af2c6d6c0048af86f1bc595acb379b5d
BLAKE2b-256 be0166be2868d83ce57f4e381efe9dfdbabad9ba77d4072d27689b30204bae46

See more details on using hashes here.

Provenance

The following attestation bundles were made for logveil-2.2.0.tar.gz:

Publisher: python-publish.yml on MeGaNeKoS/logveil

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

File details

Details for the file logveil-2.2.0-py3-none-any.whl.

File metadata

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

File hashes

Hashes for logveil-2.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ad0dc11b5c4cc29970f40a61b808215279cc8364d18eb0686b14bbed0708378b
MD5 e3039dfcf48a45874d85d7d4b3c8727f
BLAKE2b-256 35f9bca04254702b56932ac20583754574e78433bcf83c54726708d6709f7aa8

See more details on using hashes here.

Provenance

The following attestation bundles were made for logveil-2.2.0-py3-none-any.whl:

Publisher: python-publish.yml on MeGaNeKoS/logveil

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