JSONL¶
One profile per query, appended to a .jsonl file that the
profile viewer opens.
Use it when¶
- You want to see why a query is slow: both plans, every counter, per node.
- You want to compare runs of the same query, before and after a change.
- Nothing should leave the machine: there is no network involved.
Set up¶
import polars_telemetry
from polars_telemetry.export.file import FileExporter
polars_telemetry.install(exporter=FileExporter("profiles/session.jsonl"))
To collect only a block of code instead of the whole process, use
profile() and session.write(path),
which writes the same format.
What you get¶
One JSON object per line, appended as each query finishes. A file cut off mid-write still reads up to the last complete line.
{
"schema": "polars-telemetry/profile@1",
"polars_version": "1.44.2", "polars_telemetry_version": "0.2.0",
"query_id": "01a10139-b376-79e1-a019-fdd89782ff8f",
"label": "nightly/revenue_by_region",
"fingerprint": "f7d144838d88",
"started_unix_ns": 1791021921142995890,
"wall_ms": 20.49, "cpu_ms": 81.71, "result_rows": 4,
"call_site": { "filepath": "/srv/app/reports.py", "lineno": 23,
"function": "revenue_by_region" },
"failed": null, "redacted": null,
"trace_id": "...", "span_id": "...",
"diagnostics": { "parallel_efficiency": 0.33, "morsel_skew": 1.46, ... },
"plan": { "physical": [ ... ], "logical": [ ... ] }
}
| Field | What it is |
|---|---|
schema |
The format and its version; readers check it |
polars_version, polars_telemetry_version |
What produced it; polars' counters change independently of the format |
label |
From label(), else null |
fingerprint |
The plan's shape, without literals: equal for runs of the same query |
call_site |
The file, line and function that ran the query, else null |
trace_id, span_id |
Present when a span was active, linking the profile to its trace |
redacted |
What was masked before writing, such as ["strings", "numbers"], else null |
diagnostics |
Derived figures, as on the span |
plan.physical |
Physical nodes with their properties, role, and all 19 counters |
plan.logical |
The logical plan's nodes, with your own column names |
Options¶
| Option | Default | Effect |
|---|---|---|
path |
required | The file; its directory is created |
max_bytes |
64 MiB | Past this, the file moves to <name>.1 and a new one starts |
At most about twice max_bytes is on disk: the current file and one previous.
Masking is set on install()'s config, or for this file alone with
redacted(FileExporter(...), Redaction()).
Your data¶
Everything the plans contain, literals included, which is what makes a profile useful. The file stays where it is written; the viewer reads it in the browser and uploads nothing. See Data and privacy.
Cost¶
About 0.1 ms per query on a small plan and 0.35 ms on a 22-node one, on the thread that ran the query. A profile is 2–25 KB.
When it fails¶
A profile that cannot be built or written is skipped. The first such error is logged; the exporter keeps trying on the next query, so a full disk or a missing permission recovers once fixed.