spacr.crashreport¶
Create an attachable diagnostic bundle with spacr-crashreport.
A bug report that says “it crashed” costs a round trip to ask for the log, a
second one for the settings, a third for the versions, and by then the user has
re-run the pipeline and the evidence is gone. This module produces one .zip
the user can drag into a GitHub issue, and it is assembled out of the pieces
spaCR already keeps rather than collected a second time:
spacr.doctorruns 17 checks over the installation — the running checkout, duplicate installs, the GPU, the Cellpose version, the project database, the settings — and every non-PASSrow already carries the fix. Including it avoids asking support to re-derive what one command already knows.spacr.runctxwrites a per-run JSONL log keyed by run id, so “show me everything from the run that produced this” has an answer. The bundle carries that run’s log verbatim, not a filtered summary of it.spacr.errorsstamps each run’s status onto the artifact it wrote, so the report can say whether the last run finished — which is a different question from whether it crashed just now, and the more useful one when a user reports numbers rather than a traceback.
Design rules, all of them load-bearing:
Nothing here may raise. A crash reporter that crashes while reporting a
crash destroys the only evidence there was. Every collector runs through
_collect(), which turns a failure into a manifest entry naming what could
not be gathered and why. This is the one place in the codebase where catching
Exception and carrying on is the correct behaviour rather than a swallowed
error, and the reason it is correct is that the failure is recorded in the
output — the manifest is part of the bundle, so a missing section is visible
to the report recipient instead of silently absent.
Bounded size. spacr.log rotates but a single run can still write
hundreds of megabytes. The bundle takes the tail of it, capped by
MAX_LOG_BYTES, and the manifest records how much was dropped. An
attachment nobody can upload is not evidence.
Nothing secret. Environment variables are included because SPACR_*,
CUDA_* and PATH explain a large fraction of “works here, not there” —
but a value whose name looks like a credential is replaced with
"<redacted>" and listed by name in the manifest, so you can see what was
withheld and the report recipient can see that something was. Absolute paths are
kept: they name the checkout, the plate and the database, and a report with
them stripped cannot be acted on.
Deterministic layout. File names and ordering remain stable so repeated reports are quick to inspect.
Usage¶
python -m spacr.crashreport # last run, current folder
python -m spacr.crashreport --run-id 3f9c1a2b # one particular run
python -m spacr.crashreport --db plate1/measurements/measurements.db \
--settings measure.csv --app measure -o ~/spacr-bug.zip
from spacr.crashreport import report_exception, install_excepthook
install_excepthook() # any unhandled crash writes a bundle
try:
measure_crop(settings)
except Exception as exc: # noqa: BLE001 - reporting, then re-raising
print(report_exception(exc, settings=settings))
raise
Classes¶
Everything gathered, before it is written anywhere. |
Functions¶
|
Return the |
|
Gather everything worth attaching to a bug report. |
|
Return the id of the most recent run this machine logged. |
|
Write a crash report on any unhandled exception, then behave as before. |
|
Command-line entry point. |
|
Write a crash report about |
|
Gather a report and write it as one zip. |
Module Contents¶
- class spacr.crashreport.CrashReport[source]¶
Everything gathered, before it is written anywhere.
Kept as data rather than written straight to a zip so that a caller — a test, the Qt layer, a support script — can inspect or re-render it without a temporary file, and so that
write_crash_report()has nothing in it but serialisation.- Parameters:
created_utc – when the report was made, ISO-8601.
run_id – the run it is about, empty when none could be identified.
sections – file name to text, exactly as it will appear in the zip.
manifest – what was collected, what was not, and why. Every entry that failed carries the exception text; every entry that was truncated carries the byte counts.
- summary() str[source]¶
One short block naming the run, the versions and what is missing.
This is also
summary.txt, the first file in the bundle, because a report recipient opening a zip should not have to guess which file to read first.- Returns:
the summary text.
- property omitted: List[str][source]¶
Names of the sections that had nothing to collect.
No settings file was named, the project has no database, the run wrote no warnings. Each is a fact about the invocation rather than a failure, and each is still listed, because the report recipient must be able to tell “there were no warnings” from “the warnings were not gathered”.
- Returns:
section names, in collection order.
- property problems: List[str][source]¶
Names of the sections that failed while being gathered.
Distinct from
omitted, and the distinction matters: a section that had nothing to collect is an ordinary answer, and listing the two together under one alarming heading would train a reader to skip both.- Returns:
section names, in collection order. Empty on a clean report.
- spacr.crashreport.build_parser() argparse.ArgumentParser[source]¶
Return the
spacr-crashreportargument parser.- Returns:
the parser, built separately so a test can exercise the argument surface without running a collection.
- spacr.crashreport.collect(run_id: str | None = None, *, db: os.PathLike | None = None, settings: os.PathLike | None = None, settings_values: Mapping[str, Any] | None = None, checkout: os.PathLike | None = None, app: str = '', exception: BaseException | None = None, note: str = '', probe_gpu: bool = False) CrashReport[source]¶
Gather everything worth attaching to a bug report.
Composed from
spacr.doctor,spacr.runctx,spacr.errorsandspacr.logging_utilrather than collected again, so the report cannot disagree with what those tools say when run directly.- Parameters:
run_id – the run to include. Defaults to
find_last_run_id(), which prefers an activerun_contextover the newest log on disk.db – project database, e.g.
plate1/measurements/measurements.db. Adds the doctor’s database checks and the run-status stamps.settings – settings csv/json that was run.
settings_values – the settings as a mapping, when the caller has them in memory. Takes precedence over
settings.checkout – directory the user believes they are editing; defaults to the current one, which is what makes “am I running this code” answerable.
app – app key for the settings file (
mask,measure, …).exception – the exception being reported, if any. Its traceback goes in verbatim.
note – free text from the user — what they were doing.
probe_gpu – let the doctor allocate on the GPU to prove it works. Off by default: a probe that fails would add a failure that is not the one being reported.
- Returns:
the assembled
CrashReport. Never raises; a section that could not be gathered is named inCrashReport.problems.
- spacr.crashreport.find_last_run_id(problems: List[str] | None = None) str[source]¶
Return the id of the most recent run this machine logged.
The run ids live one JSONL file per run under
spacr.runctx.runs_log_dir(), so “the last run” is the newest file there. An active run wins over it: inside arun_contextthe crash being reported is this run, not the one before it.- Parameters:
problems – optional list that anything which went wrong on the way to the answer is appended to. The reason this exists rather than a bare
return '': “no run was found” and “the run directory could not be read” are different answers, and a report that cannot tell them apart sends the report recipient looking for a run that was there all along.collect()passes one and puts it in the manifest.- Returns:
the run id, or
''when none could be identified. Never raises – a missing log directory is an ordinary answer here.
- spacr.crashreport.install_excepthook(destination: os.PathLike | None = None, **kwargs: Any) Callable[source]¶
Write a crash report on any unhandled exception, then behave as before.
Chains rather than replaces: the previous
sys.excepthookstill runs, so the traceback the user is used to seeing still appears, with one line after it saying where the bundle is.- Parameters:
destination – as
write_crash_report().kwargs – passed to
collect().
- Returns:
the hook that was installed, so a caller can restore
sys.excepthookto what it was.
- spacr.crashreport.main(argv: Sequence[str] | None = None) int[source]¶
Command-line entry point.
python -m spacr.crashreport.- Parameters:
argv – arguments, defaulting to
sys.argv[1:].- Returns:
0when the bundle was written,1when it could not be — and never anything else, because a crash reporter that exits non-zero for a section it could not gather would train people to ignore it.
- spacr.crashreport.report_exception(exception: BaseException, destination: os.PathLike | None = None, **kwargs: Any) str[source]¶
Write a crash report about
exceptionand return where it went.The call to make from an
exceptblock that is about to re-raise.- Parameters:
exception – the exception being reported.
destination – as
write_crash_report().kwargs – passed to
collect().
- Returns:
the path written, or
''when even writing the file failed – which is reported on stderr rather than raised, because this is called from a failure path and must never replace the user’s exception with one of its own.
- spacr.crashreport.write_crash_report(destination: os.PathLike | None = None, **kwargs: Any) str[source]¶
Gather a report and write it as one zip.
- Parameters:
destination – where to write. A directory gets
spacr-crashreport-<run id or timestamp>.zipinside it; anything else is used as the file name. Defaults to<log dir>/spacr-crashreport-<...>.zip, which exists and is writable on every machine spaCR has ever logged on.kwargs – passed straight to
collect().
- Returns:
the absolute path of the file written.
- Raises:
OSError – only when the destination itself cannot be written, which is the one failure this function must not hide – a report the caller is told about but which is not on disk is worse than an error.
Example
from spacr.crashreport import write_crash_report path = write_crash_report(db='plate1/measurements/measurements.db') print(f'attach {path} to the issue')
Nested helpers¶
- _doctor_sections.gather_json() str¶
Return retained Doctor rows as JSON, or raise if collection failed.
spacr/crashreport.py:460
- _doctor_sections.gather_text() str¶
Run Doctor, retain its rows and summary, and return formatted text.
spacr/crashreport.py:442
- _main_log_section.gather() str | None¶
Return the capped main-log tail and record its path and size facts.
spacr/crashreport.py:575
- _run_log_section.gather() str | None¶
Return the capped run-log tail and record its path and size facts.
spacr/crashreport.py:475
- _run_status_section.gather() str | None¶
Return project run-status rows as JSON when a database provides them.
spacr/crashreport.py:555
- _run_summary_section.gather() str | None¶
Return WARNING-and-higher run records as readable text, if any.
spacr/crashreport.py:499
- _settings_section.gather() str | None¶
Return explicit or recorded settings as JSON, or
Noneif absent.spacr/crashreport.py:525