Tee_Logger
A Python logger that writes to a dated log file and optionally mirrors output to stdout with ANSI colors. Supports in-place compression, automatic log archival, and smart caller line attribution.
Features
- Tee logging — file-only (
debug,info,warning,error,critical,exception, …) or stdout+file (teeprint,teewarning,teeerror,teeexception, …) - Dated layout —
{programName}_log/YYYY-MM-DD/{programName}_YYYY-MM-DD_HH-MM-SS.log, rolling over to a new day at midnight - Caller attribution — log lines include abbreviated
file:lineof the direct caller - Compression — write
.zst/.xz/.gz/.bz2logs directly (crash-safe, see below), or tar.xz archive old day-folders - Maintenance — auto-compress and delete logs by age, in a background thread
Installation
pip install tee-logger
No runtime dependencies; src/Tee_Logger.py also works when copied on its own.
Requires Python 3.6+. In-place zstd compression requires Python 3.14+ with zstd support in the build; otherwise it falls back to xz.
Quick start
from Tee_Logger import teeLogger
tl = teeLogger(systemLogFileDir='.', programName='MyApp')
tl.info('file only')
tl.teeprint('stdout and file')
tl.teeerror('error to both')
Compressed logs:
tl = teeLogger(
programName='MyApp',
in_place_compression=True, # first available of zstd, xz, gzip, bz2; or name one
compression_level=3, # optional, backend-specific
)
Compressed logs are written in batches so they compress well, and flushed to disk:
- immediately for
WARNINGand above (warning,error,critical,exception, …), - otherwise at most
flush_intervalseconds (default 30) after a record is written, even if the program goes quiet.
So if the process is killed (OOM, SIGKILL, segfault), at most the last flush_interval seconds of DEBUG/INFO records are lost; warnings and errors are already on disk. gzip and zstd flush mid-stream; xz and bz2 cannot, so each flush finishes the current stream and starts a new one in the same file (xz -dc, bzip2 -dc, lzma.open, bz2.open read them all). A file that is still being written may end in an unfinished stream: zcat/zstd -dc print everything and then warn about it. flush_interval=0 flushes every record, which costs a lot of compression (gzip/zstd files about twice as large).
Disable file logging:
tl = teeLogger(noLog=True)
Configuration
| Parameter | Default | Description |
|---|---|---|
systemLogFileDir |
'.' |
Root directory for log folders; on failure falls back to a private teeLogger-<uid>/ (mode 0700) under the system temp dir |
programName |
caller file name without extension | Log file prefix and stdlib logger name (see below) |
suppressPrintout |
not stdout.isatty() |
Hide console output from tee* / printTable and the Log file: banner; file-only methods never print |
disable_colors |
auto | Plain [LEVEL] msg output instead of ANSI colors; by default only when the stream is not a terminal |
callerStackDepth |
-1 |
Auto-resolve direct caller; use 0+ for manual stack offset |
in_place_compression |
None |
zstd/zst, xz/lzma, gzip, bz2, or True (first available, in that order) |
compression_level |
backend default | gzip/bz2 compresslevel, xz preset, zstd level (xz presets 7–9 need hundreds of MB of RAM) |
flush_interval |
30 |
Max seconds before compressed records reach disk; 0 = every record, float('inf') = only on WARNING+ and at close |
binary_mode |
True |
Binary append mode for log files |
compressLogAfterMonths |
2 |
Archive day-folders older than N months (0 = off) |
deleteLogAfterYears |
2 |
Delete day-folders older than N years (0 = off) |
noLog |
False |
Disable file logging |
fileDescriptorLength |
15 |
Width of file:line in log records |
encoding |
'utf-8' |
Log file encoding |
collapse_single_day_logs |
auto | One file per day when compressing in-place |
Per-call override: tl.info('msg', callerStackDepth=3).
Shared logger name
programName is passed to logging.getLogger(programName). A second teeLogger with the same name reuses that logger and replaces the previous file handler so only one active log file is attached. That is intentional (one live log per name). Use distinct programName values if two components need independent log files at the same time.
Log layout
MyApp_log/
├── MyApp_latest.log # symlink to active log (Unix); .zst/.xz/.gz/.bz2 suffix when compressed
├── 2024-11-30.tar.xz # archived day-folder
└── 2025-02-10/
└── MyApp_2025-02-10_01-33-26.log
A process that runs past midnight closes its file and continues in the new day's folder; the first line there names the file it continued from.
Log files are opened without following symlinks, so a link planted at a predictable log path cannot redirect writes into another file. Still, avoid pointing systemLogFileDir (or running with the default .) at a directory other users can write to.
The logger's own messages (the Log file: … banner, fallbacks, cleanup failures) go to stderr, so they never mix into a tool's piped stdout. The banner is hidden when suppressPrintout is on.
Log line format:
2025-02-10 01:33:26,623 [INFO ] [MyApp:42 ] message text
Caller stack depth
With the default callerStackDepth=-1, teeLogger skips its own frames and records the direct caller — your code or your wrapper, not internal Tee_Logger methods.
def my_wrapper(msg):
tl.info(msg) # log shows this line
def business():
my_wrapper('hello') # log shows my_wrapper, not business
Use a non-negative callerStackDepth or a per-call override for advanced tuning.
Log maintenance
On initialization and at each midnight rollover, {programName}_log/ is scanned for YYYY-MM-DD folders (age comes from the folder name):
- Delete folders and archives older than
deleteLogAfterYears - Compress folders older than
compressLogAfterMonthsto.tar.xz
Folders with a file modified in the last 24 hours are left alone, in case another process is still writing there. The work runs in a background thread; the program exits only after an archive in progress is finished. cleanup_old_logs() runs the same maintenance synchronously.
Compression uses system tar+xz when available, otherwise Python's tarfile. It is safe against crashes and concurrent cleanups: the folder is first renamed to YYYY-MM-DD.compressing-<pid>/, the archive is written to a temporary file and renamed into place, and only then is the folder deleted. If archiving fails, the folder is put back; a claim left by a crashed process is picked up again after 24 hours.
API reference
Full docstrings and examples live in the module:
python -c "import Tee_Logger; help(Tee_Logger.teeLogger)"
Logging methods (all accept callerStackDepth=):
| File only | File + console |
|---|---|
debug |
teedebug |
info, ok |
teeprint, teeok |
warning |
teewarning |
error |
teeerror |
critical |
teecritical |
exception (adds traceback) |
teeexception (traceback in file, one-line summary on console) |
log(msg, level) |
teelog(msg, level), printTable(data) |
Public helpers: abbreviate_filename, pretty_format_table, printWithColor, getCallerInfo, compress_folder, teeLogger.
Testing
Primary entry point (pytest, including doctests in src/Tee_Logger.py):
uv sync --extra dev
uv run pytest
Secondary entry points:
python src/Tee_Logger.py --test
python src/Tee_Logger.py -V
CI runs on Python 3.9, 3.11, 3.12, and 3.14, plus 3.6 in a python:3.6 container.
Upgrading to 6.43 from 6.42
- When a compressed log file will use it (
in_place_compressionset,noLogoff, and a log directory other than/dev/null,/dev/stdoutor/dev/stderr),flush_intervalmust be a number: NaN raisesValueErrorand a non-number (None,'30') raisesTypeErrorwhen the logger is made, and the value is kept as a float. 6.42 accepted them:Noneand'30'then printed a logging error for every record below WARNING, and NaN kept its flusher thread busy on a CPU core while records reached disk only on WARNING and above and at close. Otherwise the value is still not checked. flush_interval=float('inf')no longer prints a traceback from the flusher thread; compressed logs then reach disk on WARNING and above and at close, as in 6.42.- Console and diagnostic lines are written with one
write()each instead of throughprint(), so lines from several threads no longer interleave, and a program that replacesbuiltins.printno longer changes tee-logger's output. - Fixed: a failed background flush is reported once instead of stopping the flusher for good (gzip and zstd then retry it; with xz and bz2 the records of the stream that failed are still lost); a failed close of the old log at midnight, or a stderr that cannot be written, no longer makes a log call raise or leaves the logger without a log file; records other threads log during the midnight switch are no longer lost; a signal handler that logs while its own thread is rolling the log over at midnight no longer deadlocks the process.
Upgrading to 6.42 from 6.40 or earlier
- The default
programNamedrops the file extension:myscript.pynow logs tomyscript_log/instead ofmyscript.py_log/. PassprogramName='myscript.py'to keep the old folder, or move it. in_place_compression=Truepicks zstd on Python 3.14+ (previously always xz), so the file suffix may change.compression_levelnow takes effect; it was silently ignored before.- The
Log file:banner and other internal messages moved from stdout to stderr. python-dateutilis no longer a dependency.- When the log directory is not writable, logs fall back to
$TMPDIR/teeLogger-<uid>/instead of/tmpdirectly.
License
GPL-3.0-or-later
Metadata
Release files for tee-logger 6.43
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| tee_logger-6.43.tar.gz | 57.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tee_logger-6.43-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 94.3 kB
Release files / tee_logger-6.43.tar.gz
| Download URL | tee_logger-6.43.tar.gz |
|---|---|
| Size | 57.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a7099772fca7bb73e9846e40a6554565066d3bf4b8a9c32e058080e621c1aa9c
|
|
BLAKE2b-256 checksum How to use checksums |
93e9da8d0aeb3bf03b8e99f8f6e5f5772e5db77c2b57f5623748d8d94644f4a7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / tee_logger-6.43-py3-none-any.whl
| Download URL | tee_logger-6.43-py3-none-any.whl |
|---|---|
| Size | 36.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
02dfb071323aa01f31b34a8b88b96fb0f3fa1059f5a252436bb3e5800de1eec1
|
|
BLAKE2b-256 checksum How to use checksums |
c43c3ca47dba14d34088ab5bcc81cb23c74d9db98aa6da3dd2f303729c8b0484
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for tee-logger 6.43
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.