Metadata-Version: 2.5
Name: ezpy_logs
Version: 0.2.0
Summary: EzPy Logs: swiss knife logging for personal use
Project-URL: Homepage, https://github.com/ezalos/ezpy_logs
Project-URL: Repository, https://github.com/ezalos/ezpy_logs
Author-email: ezalos <ezalos@github.com>
License: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Logging
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# EZPY Logs

Package for my personal use, swiss knife logging :
- When logging, has a nice formatter with file line and datetime (ISO 8601, UTC by default)
- Colorful logger (only on a terminal; `NO_COLOR` turns it off)
- Easy import / use: `setup()` once, `get_logger()` everywhere
- Save to files, with errors, and optionally JSON lines for machines
- Best-effort secret redaction in every sink
- Opt-in deletion of its own old logs

No runtime dependencies.

## Install

```
uv add ezpy-logs
```

## Usage

```py
import ezpy_logs

logger = ezpy_logs.get_logger(__name__)  # safe at module level: creates nothing

def main():
    ezpy_logs.setup(app_loggers=["my_package"])  # once, from the application
    logger.info("Hello from ezpy-logs!")

if __name__ == "__main__":
    main()
```

```sh
uv run python hello.py
```

Library code only calls `get_logger`. Nothing is configured and no file is written until
the application calls `setup()`.

### `setup()` arguments

| Argument | Default | Meaning |
|---|---|---|
| `log_dir` | `$EZPY_LOGS_DIR`, else `.logs` | Where files go. |
| `level` | `$EZPY_LOGS_LEVEL`, else `WARNING` | Root level: every logger you don't own (urllib3, botocore...). |
| `app_loggers` | `()` | Your own logger names (usually your package); set to `app_level`. |
| `app_level` | `DEBUG` | Level of `app_loggers`. |
| `run_id` | `None` | Write `<log_dir>/<run_id>.log` (+ `.jsonl`) instead of timestamped archives. |
| `json` | `$EZPY_LOGS_JSON == "1"` | Also write JSON lines. |
| `tz` | `$EZPY_LOGS_TZ`, else `"utc"` | `"utc"` or `"local"` (with offset) for file names and text lines. JSON `ts` is always UTC. |
| `latest` | on, off with `run_id` | Also write `Latest.log` / `Latest_ERRORS.log`, overwritten by each run. |
| `retention_days` | `None` (never delete) | Delete this library's own `archive*/*.log`/`*.jsonl` older than N days. |
| `redact_values` | `()` | Literal secrets to redact (values under 8 characters are ignored). |

Calling `setup()` again with the same arguments does nothing; with different arguments it
raises `RuntimeError`.

Environment variables: `EZPY_LOGS_DIR`, `EZPY_LOGS_LEVEL`, `EZPY_LOGS_JSON=1`,
`EZPY_LOGS_TZ`, `NO_COLOR`.

### What each sink looks like

Terminal (DEBUG/INFO on stdout, WARNING and above on stderr):

```
INFO    : 2026-09-28T21:46:49.393Z      14 ms [Thread 138181901391680]                          hello.py:12   ||  Hello from ezpy-logs!
WARNING : 2026-09-28T21:46:49.393Z      14 ms [Thread 138181901391680]                          hello.py:13   ||  token=[REDACTED:token] never reaches a sink
```

Files, by default:

```
.logs/archive/2026-09-28T21-46-49Z_1428568.log          everything
.logs/archive/2026-09-28T21-46-49Z_1428568.jsonl        everything, when json is on
.logs/archive_ERRORS/2026-09-28T21-46-49Z_1428568.log   WARNING and above
.logs/Latest.log / Latest_ERRORS.log                    this run only
```

A text file line:

```
INFO 2026-09-28T21:46:49.393Z | [14] Thread 138181901391680 || [/path/hello.py:12] main || Hello from ezpy-logs!
```

A JSON line (`extra={...}` fields are merged in; `exc` holds the traceback when there is one):

```json
{"ts": "2026-09-28T21:46:49.393Z", "level": "WARNING", "logger": "__main__", "msg": "token=[REDACTED:token] never reaches a sink", "file": "/path/hello.py", "line": 13, "func": "main", "pid": 1428568, "thread": 138181901391680, "job": 7}
```

### Secret redaction is best effort

Every ezpy_logs sink redacts bearer tokens, `ya29.` Google OAuth tokens, `ghp_`/`github_pat_`,
`hf_`, `sk-`/`sk-ant-`, `AKIA` AWS keys, PEM private-key blocks, `password=`/`token=`/`secret=`
values, and the literal `redact_values`. Regexes cannot catch every secret, and a handler someone
else adds (Sentry, pytest's caplog) bypasses it: do not rely on it as a security boundary.

### With tqdm

```py
from tqdm import tqdm
import ezpy_logs

logger = ezpy_logs.get_logger(__name__)
for name in tqdm(files, file=ezpy_logs.TqdmToLogger(logger), miniters=1e4, maxinterval=float("inf")):
    ...
```

### Migrating from 0.1

`from ezpy_logs.LoggerFactory import LoggerFactory` still works until 0.3, including its
implicit setup in `./.logs` (it emits a `DeprecationWarning` once). A `setup()` call always
replaces a `LoggerFactory` setup. See `CHANGELOG.md` for every behaviour change.

## Dev

### Local env setup

```sh
uv sync
source .venv/bin/activate
```

### TESTING

```sh
uv run pytest -v
```

### Publishing

Change version in `pyproject.toml`, add a `CHANGELOG.md` entry. `UV_PUBLISH_TOKEN` in `.envrc`
is a `pass://` ref (Agent vault, item `PIPY_TOKEN`), so publish through `secrets run`:

```sh
rip dist
uv build && secrets run -- uv publish
```
