Metadata-Version: 2.4
Name: python-devlog
Version: 2.1
Summary: No more logging in your code business logic with decorators
Author-email: めがねこ <neko@meganeko.dev>
License-Expression: MIT
Project-URL: Homepage, https://github.com/MeGaNeKoS/devlog
Keywords: clean code,decorators,logging
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE.txt
Dynamic: license-file

[![GitHub latest version](https://img.shields.io/github/v/release/MeGaNeKoS/devlog?style=for-the-badge)](https://github.com/MeGaNeKoS/devlog/releases/latest)
[![Tests](https://img.shields.io/github/actions/workflow/status/MeGaNeKoS/devlog/python-test.yml?label=Tests&style=for-the-badge)](https://github.com/MeGaNeKoS/devlog/actions/workflows/python-test.yml)
[![Publish](https://img.shields.io/github/actions/workflow/status/MeGaNeKoS/devlog/python-publish.yml?label=Publish&style=for-the-badge)](https://github.com/MeGaNeKoS/devlog/actions/workflows/python-publish.yml)
![Size](https://img.shields.io/github/repo-size/MeGaNeKoS/devlog?style=for-the-badge)
![License](https://img.shields.io/github/license/MeGaNeKoS/devlog?style=for-the-badge)

devlog
=====

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

```bash
pip install python-devlog
```

How to use
----------

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

```python
import logging
from devlog 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:

```python
import asyncio
import logging
from devlog 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:

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

```python
import logging
import requests
from devlog 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:

```python
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 devlog redacts it
from both the log message and stack trace locals:

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

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

devlog 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:** devlog 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.

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

```python
import logging
from devlog 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 devlog can do for you
---------------------------

### Decorators

devlog 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.

```python
import logging
from devlog 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, devlog 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 devlog 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:

```python
import devlog

devlog.system_excepthook_overwrite()  # Overwrite the default exception hook
```

This replaces `sys.excepthook` with devlog'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` |
