This module sets up a dual-handler logger: a console handler for humans and a file handler that writes one JSON object per line, conforming to the schema at audit_log_schema.json next to this file. The file is the compliance-relevant sink (EU AI Act Article 12, IEC 62443-4-2 SAR 6.1, IEC 61508-3 §7.4.7); the console handler is plaintext for interactive use.
Schema versioning rule: SCHEMA_VERSION is loaded directly from audit_log_schema.json at import time, so there is exactly one source of truth. Any change to the schema file is guarded by the CI job audit-log-schema-gate (.github/workflows/ci.yml), which rejects a pull request that touches the schema without a Conventional-Commits breaking-change marker (type!:) on at least one commit. That marker cascades to a MAJOR semantic-release bump.
Format LogRecord instances as single-line JSON per audit_log_schema.json.
JsonAuditFormatter
manager.logger.JsonAuditFormatter()
Format LogRecord instances as single-line JSON per audit_log_schema.json.
The formatter emits exactly the fields named in the schema’s properties section, never more. Callers pass optional structured context through the standard loggingextra= mechanism; recognised extras are event, task, and context.
Configure dual-handler logging for safety-critical execution.
Attaches a stream handler (stdout, human-readable plaintext) and, when log_dir is provided, a file handler that writes JSON records per audit_log_schema.json. The console handler honours level; the file handler is always at INFO so the audit trail stays complete even when the operator silences the console.
Optional directory for the audit log file. If provided, a timestamped task_safe_n_to_1_YYYYMMDD_HHMMSS.log file is created and receives JSON-formatted records.
If log_dir is provided but the directory cannot be created or the audit log file cannot be opened. In a safety-critical workflow a missing audit trail is not recoverable — the failure surfaces immediately rather than degrading silently to console-only logging.
Examples
import jsonimport loggingimport tempfilefrom pathlib import Pathfrom spotforecast2_safe.manager.logger import setup_logging# Reset the named logger so the example is idempotent when the notebook# kernel re-runs this cell.named = logging.getLogger("task_safe_n_to_1")named.handlers.clear()with tempfile.TemporaryDirectory() as tmp: log_dir = Path(tmp) logger, log_path = setup_logging(level=logging.WARNING, log_dir=log_dir)assert log_path isnotNoneassert log_path.exists() logger.info("pipeline started", extra={"event": "task_start"}) lines = [l for l in log_path.read_text(encoding="utf-8").splitlines() if l]assertlen(lines) >=1 record = json.loads(lines[0])assert record["event"] =="audit_log_init"print(f"log file created: {log_path.name}")print(f"first record event: {record['event']}")# Tear down so subsequent cells start cleannamed.handlers.clear()
log file created: task_safe_n_to_1_20260602_232925.log
first record event: audit_log_init