pflog
Time any function or block, and get back a record you can log, parse and query.
pflog puts a span around a call: when it began, when it ended, how long it took, on which host, and whether it succeeded. The span is a small typed record, written by default as one line of JSON on standard error, so a log collector indexes it without being told the format and pflog read turns a day of them back into a table.
from pflog import timed
@timed("convert")
def convert(inputFile: str, outputFile: str) -> int:
...
return 0
convert("in.dcm", "out.nii.gz")
{"duration": 4.213, "endedAt": "2026-10-14T16:02:11-04:00", "errors": [], "host": {...}, "name": "convert", "returncode": 0, "startedAt": "2026-10-14T16:02:07-04:00", "status": true}
The gap
Every long-running program ends up wanting to know how long its main step took, and the answer is usually a print of a subtraction between two timestamps. That line is prose: a collector cannot query it, a second program cannot parse it, and it says nothing about whether the step worked. Platform telemetry sees the other side, pods and containers and their resource use, but it cannot know what the program meant by a step, and the volume of what it does know becomes an obstacle to finding the one number that mattered.
pflog is the small piece in between: a record shaped by the program, emitted where the platform already looks. The program names the span and nothing else. The record carries the outcome, so a failed call is a failed span, not a number that looks like every other.
The record
A span has these fields. The last six are the same fields, in the same formats, as hostlib keeps for a process, so a log of spans and a log of jobs read as one.
name |
The span’s name; the function’s name when none is given. |
status |
True when the call returned without error and, if it returned an exit status, that status was 0. |
returncode |
The integer the call returned, or the code of a SystemExit it raised; None otherwise. |
errors |
Why a failed span failed, in words: "ValueError: bad input". |
host |
Hostname, user, pid, platform, working directory. |
startedAt |
When it started, ISO 8601 with the local offset. |
endedAt |
When it ended. |
duration |
Seconds, from a monotonic clock. |
The outcome is read from the call the way a shell reads a program. An integer returned, or a SystemExit raised, is an exit status with 0 meaning success. A boolean is the status itself. Anything else returned without error is success. An exception is a failure, recorded and then raised on as if the span were not there.
Timing a function
The decorator takes a name and, optionally, a reporter. The function’s return value and exceptions pass through untouched.
from pflog import timed, FileReporter
@timed("recon", report=FileReporter("/out/spans.jsonl"))
def recon(subject: str) -> int:
...
Used bare, @timed names the span after the function and reports to standard error.
A command-line program’s main is the span that matters most. For a ChRIS plugin, the span goes on standard error, where CUBE keeps it with the rest of the plugin’s output.
@chris_plugin(...)
@timed("shexec")
def main(options: Namespace, inputdir: Path, outputdir: Path) -> int:
...
Timing a block
The context manager times a block and gives the open span a handle, for noting an error the block survived or an outcome the block knows better than the library.
from pflog import span_open
with span_open("slices") as span:
for slice_ in slices:
if not slice_.process():
span.error_add(f"{slice_.name} was empty")
A span with errors is a failed span, however the block ended.
Where the record goes
A reporter is anything with a report(span) method. Five come with the library.
JsonReporter(stream) |
One line of JSON per span; the process’s standard error by default. |
FileReporter(path) |
One line of JSON per span, appended; the directory is created. |
LogReporter(logger) |
recon: 4.213 s, ok through Python’s logging, for programs that already log. |
TeeReporter(a, b) |
Every span to each reporter given. |
NullReporter() |
Nothing. |
Standard error is the default on purpose. A program’s standard output may be data that another program parses; its standard error is where a line about the run belongs, and it is captured by every container runtime and log collector.
Reading it back
The pflog command reads files of JSON lines, span lines and hostlib job lines alike, and prints one row each.
pflog read /out/*/spans.jsonl
pflog read --failed /out/*/spans.jsonl
pflog read --json --failed /out/*/spans.jsonl | jq .errors
STARTED DURATION STATUS NAME
2026-10-14T09:14:51-04:00 0.306 ok demo
2026-10-14T09:14:51-04:00 0.002 failed demo
2026-10-14T09:14:52-04:00 0.003 failed wc -l /nonexistent
pflog demo --file /tmp/spans.jsonl times a sleep and reports it, for seeing a span without writing code. pflog --man is the full manual.
What pflog is not
It is not a transport. A span is written where the program’s output already goes, and whatever collects that output carries it on; the library opens no sockets and posts to no server. It is not a profiler: one span per call, named by the program, not a trace of every function. It is not a logger: LogReporter writes through Python’s logging when a program wants spans in its log, but the library has no levels, no handlers and no configuration of its own.
Install
pip install pflog
Python 3.11 or later. One dependency, hostlib, for the host facts and the record formats shared with it.
Layout
pflog/
├── models/ Span, and hostlib's records re-exported
├── span/ timed, span_open, SpanBuilder
├── report/ JsonReporter, FileReporter, LogReporter, TeeReporter, NullReporter
└── cli/ the pflog command: read, demo, --man
docs/purpose.adoc says why the library exists, docs/architecture.adoc how it is built, docs/history.adoc where it came from.
Metadata
Release files for pflog 2.0.0
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 | |
|---|---|---|---|---|
| pflog-2.0.0.tar.gz | 23.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pflog-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 45.4 kB
Release files / pflog-2.0.0.tar.gz
| Download URL | pflog-2.0.0.tar.gz |
|---|---|
| Size | 23.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a366205243b88a6c01f78997a50f28149bed9fe85588ba6e8e95b6f5f2613b72
|
|
BLAKE2b-256 checksum How to use checksums |
79dc99f069570d00ccc96d0fd18f7589c9a032ffe5e590dbe7c4e34054b7d3dd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / pflog-2.0.0-py3-none-any.whl
| Download URL | pflog-2.0.0-py3-none-any.whl |
|---|---|
| Size | 22.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ce73bec7f69f0b57528a1b1c567fa3477320d91a66d2cd9be34748e6cfd23d09
|
|
BLAKE2b-256 checksum How to use checksums |
c7ba2ed7efdc8bb5853e59cd772fd4dafc26988f139f54a425ce339783448b04
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logSecurity for pflog 2.0.0
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pflog-2.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logA 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.