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. A batch goes to disk:
- immediately for
WARNINGand above (warning,error,critical,exception, …), - otherwise at most
flush_intervalseconds (default 30) after a record is logged, even if the program goes quiet, or as soon as 1 MiB of records is waiting.
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. Each batch is written as a whole gzip member (xz or bz2 stream, zstd frame), and zcat, xz -dc, bzip2 -dc, zstd -dc and Python's readers read the members back as one file. So a log stays readable however its writers end, including when a restarted run or a second process with the same programName appends to the same day's file.
Known limitations (details in docs/limitations.md):
- Each batch is compressed on its own, so frequent flushes make gzip and zstd logs larger: about +40% when one record in ten is
WARNINGor above, three times withflush_interval=0. Waiting records are written before each fork (Python 3.7+), which adds a member per fork. xz and bz2 are not affected. - A batch that cannot be written (a full disk) is lost, and each failed write is reported on stderr.
binary_mode=Falsedoes not apply to compressed logs.- If the background flusher cannot be started (the process is out of threads), it is not tried again; records then reach disk on
WARNINGand above, on a later record, and at exit. - On Python 3.6 and 3.7, when records fall back to stderr, another thread's output can land inside a record's line.
- A forked child that leaves with
os._exitor is killed loses the records it logged since its last write; so can the workers of a Pool ended byterminate()(itswithblock). Workers that end normally do not. - On Python 3.6, records waiting at a fork are not written first: a daemon whose parent leaves with
os._exitloses its startup lines. And, as with any 3.6 logging handler, a child forked while another thread is writing the log can hang at its first record.
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+, per 1 MiB of records, and at close |
binary_mode |
True |
Binary append mode for uncompressed log files; compressed ones are always binary |
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. A folder named like a date that is not one (2020-13-45) is skipped, with a message on stderr. 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.44 from 6.43
- Compressed logs are written as one whole gzip member (xz or bz2 stream, zstd frame) per flush. In 6.43 a gzip or zstd log could not be read past the point where a run was killed, once a restarted run or a second process with the same
programNameappended to the same day's file; such a file now stays readable. The cost: gzip and zstd logs with frequentWARNING+ records, or withflush_interval=0, are larger (see the limitations above). xz and bz2 logs are unchanged. - A compressed batch that cannot be written (a full disk) is dropped and reported each time it fails; a partial write is cut back so later records stay readable.
binary_mode=Falseno longer affects compressed logs.teeLogger.GZipFileHandler,BZ2FileHandler,XZFileHandlerandZSTDFileHandlerare gone; one internal handler writes all four formats.teeLogger.BinFileHandlerstays.flush_intervalis taken withfloat():'30'now means 30 seconds (6.43 raisedTypeError), and NaN means "only onWARNINGand above and at close" (6.43 raisedValueError).- Console lines go through
print()again, still with each whole line in one write, so a program that replacesbuiltins.printsees them. - At midnight the log handler switches to the new day's file itself. A signal handler that logs while its thread holds
logging's own lock can no longer deadlock with another thread's midnight switch. - A
logging.FileHandleryou attach yourself to the logger namedprogramNameis no longer closed when anotherteeLoggerof that name is made. - If the background flusher cannot be started, it is not tried again (6.43 retried once per interval).
- Compressed records still waiting when the process forks are written before the fork (Python 3.7+; 3.6 has no at-fork hooks), so a daemon's parent leaving with
os._exitno longer drops them. A child no longer writes the parent's waiting records a second time, and starts a flush timer of its own. A multiprocessing worker writes its last records when it ends, also when it imports Tee_Logger only once it runs. 6.43 duplicated such records, or corrupted gzip and zstd logs, and lost a worker's last ones. A program that forks often while records wait gets one more compressed member per fork. A child forked while another thread was switching to the new day's file no longer hangs on that switch's lock at its own next midnight (on 3.6, see the limitation above). - The
tee*methods log to the file before they print, so a console that fails (prog | head -1) no longer loses the record from the file; the error is still raised. - A compression level the backend refuses is reported by name (
gzip refuses compression_level 10 (...)). - A day-folder named like a date that is not one (
2020-13-45) is skipped with a message, instead of being aged by its mtime. - The stderr fallback uses the stdlib
StreamHandler; see the 3.6/3.7 limitation above.
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.44
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 | Actions |
|---|---|---|---|---|
| tee_logger-6.44.tar.gz | 60.0 kB | Without provenance | Details for tee_logger-6.44.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| tee_logger-6.44-py3-none-any.whl | Python 3 | none | any | Details for tee_logger-6.44-py3-none-any.whl |
Total release size: 96.3 kB
Release files / tee_logger-6.44.tar.gz
| Download URL | tee_logger-6.44.tar.gz |
|---|---|
| Size | 60.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0e20924439436a96ce1733a08b656fc999517cdfdfb5e46448cc0eee8954fcbc
|
|
BLAKE2b-256 checksum How to use checksums |
2d3da0306fb9dc481a88e0657c6ebc020029b7a0366f85e1e783ab6873eb0975
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","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.44-py3-none-any.whl
| Download URL | tee_logger-6.44-py3-none-any.whl |
|---|---|
| Size | 36.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8213531bdefafde24bca543477fae3bfe6c9331b7ee6bea7902a8442826f2898
|
|
BLAKE2b-256 checksum How to use checksums |
508f3f1f0d413da15eebead626ccd15ea150f9651a50d823e2709a5220c84f6c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","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.44
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.