Universal logger with custom levels for ETL and automation processes.
Project description
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 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
pip install handsome-log
Quick Start
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")
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.
logger.startup("Pipeline started")
logger.finished("Pipeline done")
To disable the border, pass border=False:
logger.startup("Starting without border", border=False)
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 └─.
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")
Custom HEX Colors
Override the default color for any log level using HEX codes or named colors from colorlog:
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.
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.
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)
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.
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>
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:
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:
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",
)
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:
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):
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) |
# 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()
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()
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 module and the excellent colorlog package. Full credit and gratitude to the developers of those foundational libraries.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file handsome_log-1.0.0.tar.gz.
File metadata
- Download URL: handsome_log-1.0.0.tar.gz
- Upload date:
- Size: 15.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a9a8546bcb7e51ff7ba89923840e9439b16b4d473bdf088bb1cd4f30beeec595
|
|
| MD5 |
da1a4da9625b3b266665b4a04a16e892
|
|
| BLAKE2b-256 |
e194df8dcaff5f31084c8629fcd1200a07c6123957d87748495f6bcafc1bbb3c
|
File details
Details for the file handsome_log-1.0.0-py3-none-any.whl.
File metadata
- Download URL: handsome_log-1.0.0-py3-none-any.whl
- Upload date:
- Size: 13.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c835f019d23e093a832379ba910258238fb66bc2ffbee89f175c8302391a3dc
|
|
| MD5 |
d7f8c8aaa35f1471bf898307eaf591b5
|
|
| BLAKE2b-256 |
59c73e3145c7504c590a484a028e6cc1ed233e637a877bbc9d2d2810648aa6ed
|