Logging simulation diagnostics¶
If you want to save periodic diagnostic records to a file, you can configure a file handler for the
passengersim.diagnostics logger. This example shows how to do that, and how to read the resulting
JSON-lines file. It is important to remember that the logger must be configured before the
simulation run starts, otherwise your periodic records will simply be sent into the void, never to
be seen.
PassengerSim uses the passengersim.diagnostics logger and existing handlers; it does not create a
log file for you. If that logger does not accept INFO records when the run starts, periodic
diagnostic logging is not started.
This example writes one JSON object per line to a uniquely named file, runs a small two-trial model, and reads the resulting records. Logging is independent of which live dashboard panels are visible.
The usual PassengerSim installation and license requirements apply. See periodic diagnostic logging for the full configuration and record descriptions.
import json
import logging
from datetime import UTC, datetime
from pathlib import Path
from uuid import uuid4
import passengersim as pax
Configure a short demonstration run¶
The default logging interval is 60 seconds. Here we use one second to make periodic records easier to observe during a short example; a very short run may still finish in less than one second, before the first periodic record. Start, completion, and other lifecycle records are emitted regardless of the interval. For production runs, 60 seconds is usually a better starting point.
Temporary SQLite recording lets the logs include database sizes and growth without keeping the database after the result is closed. The diagnostic log file itself remains available.
cfg = pax.Config.from_yaml(pax.demo_network("3MKT/DEMO"))
cfg.simulation_controls.num_trials = 2
cfg.simulation_controls.num_samples = 500
cfg.simulation_controls.burn_samples = 50
cfg.simulation_controls.diagnostics = {"mode": "live", "log_interval": 1.0}
cfg.db = {"result_storage": "temporary", "write_items": ["leg"]}
cfg.outputs.disk = False
cfg.outputs.reports.clear()
Install the file handler before running¶
Each diagnostic LogRecord that PassengerSim will emit has a diagnostics dictionary. This formatter
saves that dictionary without rounding its numeric values and adds a UTC timestamp. The filter keeps
unrelated messages out of the JSON-lines file. Other than knowing that there will be a diagnostics
dictionary in the LogRecord, nothing in the following code block is PassengerSim-specific, it is
just regular Python logging semantics.
class DiagnosticJSONFormatter(logging.Formatter):
"""Serialize diagnostic records with an explicit UTC timestamp."""
def format(self, record: logging.LogRecord) -> str:
payload = dict(record.diagnostics)
payload["timestamp"] = datetime.fromtimestamp(record.created, UTC).isoformat()
return json.dumps(payload)
log_directory = Path("diagnostic-logs")
log_directory.mkdir(parents=True, exist_ok=True)
log_path = log_directory / f"run-{uuid4().hex[:12]}.jsonl"
diagnostic_logger = logging.getLogger("passengersim.diagnostics")
previous_level = diagnostic_logger.level
previous_propagate = diagnostic_logger.propagate
file_handler = logging.FileHandler(log_path, encoding="utf-8")
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(DiagnosticJSONFormatter())
file_handler.addFilter(lambda record: hasattr(record, "diagnostics"))
diagnostic_logger = logging.getLogger("passengersim.diagnostics")
diagnostic_logger.addHandler(file_handler)
diagnostic_logger.setLevel(logging.INFO)
diagnostic_logger.propagate = False
Once that’s all set up, we are ready to actually run a simulation, and it will log diagnostics while it’s running.
The try/finally block closes the file and restores the logger configuration even if the
simulation fails. Removing only the handler installed here also prevents duplicate handlers when the
notebook cell is rerun. If you want to re-run with logging, you’ll need to re-run both cells to
re-set the handlers for this run. Other existing handlers are left in place; propagation is
temporarily disabled to keep these records out of ancestor loggers’ console handlers.
try:
# The handler and INFO level are set in the prior cell before constructing/running the model.
with pax.MultiSimulation(cfg) as sim:
result = sim.run(max_processes=2)
try:
final_diagnostics = result._metadata["diagnostics"]
finally:
result.close()
finally:
# Clean up the diagnostic logger and file handler.
diagnostic_logger.removeHandler(file_handler)
file_handler.close()
diagnostic_logger.setLevel(previous_level)
diagnostic_logger.propagate = previous_propagate
print(f"Diagnostic records saved to {log_path.resolve()}")
╭────────── Summary revenue statistics · 3MKT ──────────╮ │ AL1 AL2 │ │ Run Samples Mean ~95% CI Mean ~95% CI │ │ 3MKT 900 90,148 ±810 90,117 ±842 │ │ Post-burn-in only · Approx. 95% CI for mean │ ╰───────────────────────────────────────────────────────╯
Diagnostic records saved to /Users/jpn/Git/pax/documentation/examples/diagnostic-logs/run-0ac46e29da94.jsonl
Inspect the log¶
The last record contains the final available snapshot and completion status. Timing values are
summed worker seconds, which can exceed elapsed wall time for parallel runs. Carrier counts exclude
burn-in; completed_samples includes it. Confidence-interval half-widths are null until 30
post-burn-in observations are available for a carrier.
Periodic records can contain older worker observations during a long operation; inspect
snapshot_age_seconds and the ages in active_trials when interpreting them. Database sizes
include each main file and its journal/WAL/SHM companions. Files may disappear as temporary
databases are consolidated or cleaned up.
Loading the whole file below is convenient for this small example. For long runs, iterate over lines instead of retaining every record in memory.
with log_path.open(encoding="utf-8") as stream:
records = [json.loads(line) for line in stream]
final_record = records[-1]
print(f"Recorded {len(records)} diagnostic events")
print(f"Final status: {final_record['event']}")
print(f"Completed samples: {final_record['completed_samples']}")
print(json.dumps(final_record, indent=2))
Recorded 11 diagnostic events
Final status: completed
Completed samples: 1000
{
"event": "completed",
"run_id": "3MKT:476c044c5115",
"trial_id": null,
"elapsed_wall_seconds": 3.2361356250003155,
"snapshot_age_seconds": 5.2458999562077224e-05,
"completed_samples": 1000,
"samples_per_second": null,
"stage": null,
"active_trials": {},
"worker_seconds": {
"core": 1.7754908331171464,
"rm": 0.8861150727443601,
"database": 0.32095987120919744,
"other": 0.7070784309289593
},
"worker_total_seconds": 3.689644207999663,
"worker_percent": {
"core": 48.120922588352414,
"rm": 24.016274274444648,
"database": 8.698938247577143,
"other": 19.1638648896258
},
"recent_worker_seconds": {
"core": 0.0,
"rm": 0.0,
"database": 0.0,
"other": 0.0
},
"recent_worker_percent": {
"core": null,
"rm": null,
"database": null,
"other": null
},
"carriers": {
"AL1": {
"samples": 900,
"mean": 90148.08333333333,
"ci95_half_width": 809.9028851655281
},
"AL2": {
"samples": 900,
"mean": 90116.80555555558,
"ci95_half_width": 841.043068733584
}
},
"databases": [
{
"path": "/private/var/folders/sb/mb_3f0t16lx0wm3tr8r5g9rm0000gn/T/passengersim-db-8hvil4nb/trial-0000.sqlite",
"bytes": 2216488,
"bytes_per_second": 2090591.6044273435,
"error": null
},
{
"path": "/private/var/folders/sb/mb_3f0t16lx0wm3tr8r5g9rm0000gn/T/passengersim-db-8hvil4nb/trial-0001.sqlite",
"bytes": 2237952,
"bytes_per_second": 2111946.5066948407,
"error": null
}
],
"failed_trials": {},
"cancelled_trials": [],
"database_timings": {
"1": {
"simulation_and_writes_s": 1.8471169589993224,
"eager_reports_s": 0.00023237499954120722,
"worker_finalization_s": 0.0004215830012981314,
"receive_delay_s": 0.004592416999003035,
"merge_s": 0.009864333000223269
},
"0": {
"simulation_and_writes_s": 1.8506013340011123,
"eager_reports_s": 0.00015487499877053779,
"worker_finalization_s": 0.0003845000010187505,
"receive_delay_s": 0.009901082999931532,
"merge_s": 0.01187716700042074
}
},
"timestamp": "2026-11-04T05:40:30.169700+00:00"
}
Adjust logging for the next run¶
Use cfg.simulation_controls.diagnostics.log_interval = 60 for the default cadence, or set it to
None to disable diagnostic logging while retaining other diagnostics. Setting
cfg.simulation_controls.diagnostics.mode = "off" disables both collection and diagnostic logging.
Changing .mode back to "live" keeps the logging interval and panel settings.
Assigning a shorthand to the entire diagnostics setting, such as "full" or "off", resets its
other options, including the interval. Set the interval afterward if needed. For another run with
file logging, rerun both the handler and run cells above so logging is configured again before the
simulation starts.