# handsome_log

`handsome_log` is a Python logger designed for ETL pipelines, automation tasks, and web scraping scripts.

Built on top of Python's standard `logging` module and the [`colorlog`](https://github.com/borntyping/python-colorlog) package, it adds custom log levels, colored output, secret masking, loop spinners, and flexible file rotation — all with minimal setup.

> The main motivation: stop copying logging snippets from old projects. One import, one line, and you're done.




---

## Installing

```bash
pip install handsome-log
```

---

## Quick Start

```python
from handsome_log import get_logger

logger = get_logger(__name__)

logger.startup("Pipeline started")
logger.info("Processing data")
logger.success("Step completed successfully")
logger.finished("Pipeline done")
```
![alt text](images/example_01.png)

---

## Log Levels

`handsome_log` includes all standard Python levels plus custom ones designed for data pipelines:

| Level        | Method              | When to Use                                              | Default Color |
|--------------|---------------------|----------------------------------------------------------|---------------|
| `STARTUP`    | `logger.startup()`  | Initializing or starting a process                       | Bold Blue     |
| `DEBUG`      | `logger.debug()`    | Detailed technical or debugging messages                 | Cyan          |
| `DRY_RUN`    | `logger.dry_run()`  | Simulation runs that don't commit any changes            | Purple        |
| `VALIDATION` | `logger.validation()`| Validating data, schemas, or credentials                | Blue          |
| `INFO`       | `logger.info()`     | General runtime information                              | Green         |
| `SUCCESS`    | `logger.success()`  | A process or step completed successfully                 | Bold Green    |
| `WARNING`    | `logger.warning()`  | Something unexpected happened, but the process continues | Yellow        |
| `ERROR`      | `logger.error()`    | A failure occurred but the system is still running       | Red           |
| `CRITICAL`   | `logger.critical()` | A critical failure that halts the process                | Bold Red      |
| `FINISHED`   | `logger.finished()` | Final message marking the end of a process               | Bold Green    |

---

## Features

### Startup and Finished Borders

`startup()` and `finished()` automatically wrap the message with `##########` borders to make pipeline boundaries easy to spot in long logs.

```python
logger.startup("Pipeline started")

logger.finished("Pipeline done")
```



To disable the border, pass `border=False`:

```python
logger.startup("Starting without border", border=False)
```
![alt text](images/example_02.png)
---

### Hints

Every log method accepts an optional `hint` keyword argument. The hint is printed on the line below the log message, indented and in the same color, prefixed with `└─`.

```python
logger.warning("Minor delay in response", hint="Consider optimizing the database query")


logger.error("Failed to write file", hint="Check if the directory exists and has write permissions")
logger.success("Step completed", hint="Results saved to output/result.json")
```
![alt text](images/example_03.png)
---

### Custom HEX Colors

Override the default color for any log level using HEX codes or named colors from `colorlog`:

```python
logger = get_logger("MY_PIPELINE", custom_colors={
    "DEBUG":      "#00BFFF",
    "INFO":       "#00FF7F",
    "WARNING":    "#FFA500",
    "ERROR":      "#FF5733",
    "CRITICAL":   "#FF0000",
    "SUCCESS":    "#FFD700",
    "STARTUP":    "#DA70D6",
    "VALIDATION": "#40E0D0",
    "DRY_RUN":    "#FF69B4",
})

logger.startup("HEX colors active")
logger.success("Success — gold")
```

Only the specified levels are overridden; all others use the defaults.

![alt text](images/example_04.png)
---

### Loop Status Spinner

`loop_status()` wraps any iterable and displays a live terminal spinner while the loop runs. When the loop finishes (or fails), it prints the elapsed time.

```python
import time

for item in logger.loop_status(range(100), message="Processing items", show_percent=True):
    time.sleep(0.05)

# [myapp] Processing items  57%  /
# → [myapp] Processing items done (Elapsed time: 5.23s)
```
<video controls src="images/for_loop_correto.mp4" title="Title"></video>


**Parameters:**

| Parameter      | Default        | Description                                        |
|----------------|----------------|----------------------------------------------------|
| `message`      | `"Processing"` | Text displayed next to the spinner                 |
| `delay`        | `0.1`          | Delay between spinner frames (seconds)             |
| `show_spinner` | `True`         | Toggle the spinning character                      |
| `show_percent` | `False`        | Show progress as a percentage (requires `__len__`) |
| `end_message`  | `"done"`       | Message printed after the loop completes           |

---

### Secret Masking

When you log messages that involve API keys, passwords, tokens, or any other sensitive value, there's always a risk of accidentally writing the raw secret to the terminal or to a log file.

`mask()` solves this by intercepting the secret **before** it ever reaches the logger. It works inside an f-string, so the raw value is evaluated in Python memory, converted to a safe placeholder, and only the placeholder is passed to the log call. **The actual secret is never stored, never written to any file, and never appears in any log output.**

```python
from handsome_log import get_logger, mask

logger = get_logger("myapp")

api_key = "sk-abc123def456xyz789"

# The raw api_key value never touches the logger — only "<masked>" does
logger.info(f"API key loaded: {mask(api_key)}")
# [12:00:00] [myapp] [INFO] : API key loaded: <masked>
```
![alt text](images/example_05.png)
You don't need to sanitize your variables beforehand or remember to strip secrets from messages. Just wrap the value with `mask()` wherever you reference it in a log call and you're safe.

**Masking styles:**

| Style     | Example output      | Description                                  |
|-----------|---------------------|----------------------------------------------|
| `full`    | `<masked>`          | Hides everything, including length (default) |
| `stars`   | `********`          | 8 fixed asterisks — also hides length        |
| `partial` | `sk-a***xyz7`       | Reveals the first and last `show` characters |

Use `partial` when you need to confirm *which* key or token is being used (e.g. to match it against a list) without leaking the full value:

```python
logger.info(f"API key: {mask(api_key)}")                             # <masked>
logger.info(f"API key: {mask(api_key, style='stars')}")              # ********
logger.info(f"API key: {mask(api_key, style='partial', show=4)}")    # sk-a***xyz7
logger.info(f"API key: {mask(api_key, style='partial', show=6)}")    # sk-abc***xyz789
```

`mask()` also works inside `hint`, so even contextual debugging info stays safe:

```python
logger.success(
    f"Connected to DB — password {mask(db_password)} accepted",
    hint=f"Using key {mask(api_key, style='partial', show=4)} — rotate every 30 days",
)
logger.error(
    f"Auth failed for token {mask(jwt_token, style='partial', show=8)}",
    hint="Check token expiration and issuer claim",
)
```
![alt text](images/example_06.png)
---

## Log Files

### Basic File Logging

Pass `log_to_file=True` and a `log_file_path` to write logs to a file simultaneously with the console output:

```python
logger = get_logger(
    "MY_PIPELINE",
    log_to_file=True,
    log_file_path="logs/pipeline.log",
)
```

The directory is created automatically if it doesn't exist.

---

### File Rotation

#### By Size

Rotate when the file exceeds a size limit (in bytes):

```python
logger = get_logger(
    "MY_PIPELINE",
    log_to_file=True,
    log_file_path="logs/pipeline.log",
    max_file_size=5_000_000,  # 5 MB
    file_retention=5,         # keep up to 5 backup files
)
```

#### By Time

Use `file_rotation` to rotate on a schedule. `file_retention` sets how many days of logs to keep.

| `file_rotation` | Rotates...                          | `file_retention` interpreted as |
|-----------------|-------------------------------------|---------------------------------|
| `"daily"`       | Every 24 h at midnight              | Days (= number of backups)      |
| `"weekly"`      | Every 7 days from creation          | Days (`// 7` = weekly backups)  |
| `"monthly"`     | At the start of each calendar month | Days (`// 30` = monthly backups)|
| `"custom"`      | Every `custom_retention` days       | Days (`// custom_retention`)    |

```python
# Daily — keep 7 days
logger = get_logger("MY_PIPELINE", log_to_file=True, log_file_path="logs/daily.log",
                    file_rotation="daily", file_retention=7)

# Weekly — keep 4 weeks
logger = get_logger("MY_PIPELINE", log_to_file=True, log_file_path="logs/weekly.log",
                    file_rotation="weekly", file_retention=28)

# Monthly — keep 3 months
logger = get_logger("MY_PIPELINE", log_to_file=True, log_file_path="logs/monthly.log",
                    file_rotation="monthly", file_retention=90)

# Custom — rotate every 3 days, keep 30 days total
logger = get_logger("MY_PIPELINE", log_to_file=True, log_file_path="logs/custom.log",
                    file_rotation="custom", custom_retention=3, file_retention=30)
```

---

## API Reference

### `get_logger()`

```python
from handsome_log import get_logger

logger = get_logger(name, **options)
```

| Parameter           | Type            | Default        | Description                                                             |
|---------------------|-----------------|----------------|-------------------------------------------------------------------------|
| `name`              | `str`           | required       | Logger name (typically `__name__`)                                      |
| `level`             | `int`           | `logging.DEBUG`| Minimum log level to capture                                            |
| `log_to_file`       | `bool`          | `False`        | Write logs to a file                                                    |
| `log_file_path`     | `str \| None`   | `None`         | Path to the `.log` file. Required when `log_to_file=True`               |
| `use_colors`        | `bool`          | `True`         | Enable ANSI color output (terminal only)                                |
| `overwrite_handlers`| `bool`          | `False`        | Clear existing handlers before adding new ones                          |
| `show_seconds`      | `bool`          | `True`         | Include seconds in the log timestamp                                    |
| `custom_colors`     | `dict \| None`  | `None`         | Override colors per level. Accepts named colors or HEX codes (`#FF5733`)|
| `file_rotation`     | `str \| None`   | `None`         | Time-based rotation: `"daily"`, `"weekly"`, `"monthly"`, `"custom"`    |
| `file_retention`    | `int`           | `30`           | Days of rotated files to keep (or backup count for size rotation)       |
| `max_file_size`     | `int`           | `0`            | Rotate by size (bytes). Only active when `file_rotation=None`. `0` = disabled |
| `custom_retention`  | `int`           | `1`            | Rotation interval in days when `file_rotation="custom"`                 |

---

### `mask()`

```python
from handsome_log import mask

mask(value, style="full", show=4)
```

| Parameter | Type  | Default  | Description                                              |
|-----------|-------|----------|----------------------------------------------------------|
| `value`   | `str` | required | The secret value to mask                                 |
| `style`   | `str` | `"full"` | `"full"`, `"stars"`, or `"partial"`                      |
| `show`    | `int` | `4`      | Characters to reveal at each end when `style="partial"`  |

---

## Credits

Built on top of Python's standard [`logging`](https://docs.python.org/3/library/logging.html) module and the excellent [`colorlog`](https://github.com/borntyping/python-colorlog) package. Full credit and gratitude to the developers of those foundational libraries.
