"""Logging handler that bridges :mod:`logging` to JSONL run events.The handler subscribes to the standard :mod:`logging` machinery so anycaller using :func:`aeat.core.logging.get_logger` automatically picksup the JSONL sink while a:func:`aeat.core.observability.run_context` is active. Records that donot carry a ``run_event`` extra are skipped — bare log lines neverleak into ``events.jsonl``.The ``run_id`` / ``step_id`` attributes are stamped onto every:class:`logging.LogRecord` by the factory installed in:mod:`aeat.core.logging`.Each sink instance is bound to a single ``run_id`` and filters anyevent whose ``run_id`` does not match. This prevents cross-runcontamination when several:func:`aeat.core.observability.run_context` blocks execute concurrently(e.g. tasks in an :mod:`asyncio` event loop) and therefore havecompeting sinks attached to the root logger at the same time."""from__future__importannotationsimportjson# LOGGING-STDLIB-RATIONALE-SINK-HANDLER:# JsonlRunSink subclasses logging.Handler and accepts logging.LogRecord; stdlib# import is required by the ABC contract.importlogging# LOGGING-STDLIB-RATIONALE-SINK-HANDLERimportosimportthreadingfrompathlibimportPathfromtypingimportTextIO,overridefrom..loggingimportget_loggerfrom._modelsimportRunEventfrom._redaction_rulesimportdiagnostic_ruleslogger=get_logger(__name__)
[docs]classJsonlRunSink(logging.Handler):"""Append-only JSONL sink for :class:`aeat.core.observability.RunEvent` records. The handler opens the target path lazily on first emit so a :func:`aeat.core.observability.run_context` enter is cheap when no events ever fire. Each emit flushes the file handle; :meth:`close` additionally calls :func:`os.fsync` so a process kill mid-run still leaves a durable JSONL trailer on disk. Concurrency: the sink is bound to a single ``run_id`` and rejects events carrying a different ``run_id``. File-handle mutations are guarded by an internal :class:`threading.Lock` so multiple worker threads may emit concurrently without interleaving bytes on disk. """def__init__(self,target:Path,*,run_id:str)->None:"""Construct a sink bound to a specific JSONL file path and run. Args: target: Path of the ``events.jsonl`` file this sink writes. The parent directory is created eagerly. run_id: The owning run identifier. Events whose ``run_id`` does not match are dropped silently so concurrent runs that share the same root logger stay isolated. """super().__init__(level=logging.DEBUG)self._target:Path=targetself._run_id:str=run_idself._handle:TextIO|None=Noneself._lock:threading.Lock=threading.Lock()target.parent.mkdir(parents=True,exist_ok=True)@propertydefrun_id(self)->str:"""The run identifier this sink is bound to."""returnself._run_id
[docs]@overridedefemit(self,record:logging.LogRecord)->None:"""Write the JSON-encoded :class:`RunEvent` carried by ``record``. Drops the record when there is no ``run_event`` extra or when the event belongs to a different run — see the module docstring for the concurrency rationale. JSON serialisation runs outside the file-handle lock so concurrent threads can encode in parallel; only the write and flush are serialised. The whole emit path (including the encode) is wrapped in a single ``try`` — a serialisation failure (e.g. the pydantic model grew a non-JSON-safe field in a future refactor) must not crash the logging system, and must instead fall through to ``handleError`` like any other handler failure. """event=getattr(record,"run_event",None)ifnotisinstance(event,RunEvent):returnifevent.run_id!=self._run_id:returntry:# Run traces are DIAGNOSTIC class. The substrate's redaction# rule set walks every string leaf (NIF SHA-256-prefixed, URL# host-only, bearer-shaped tokens fingerprinted, opaque# bearers fingerprinted) before serialisation so the JSONL# never carries a plaintext NIF / token / URL path even if# a caller feeds one in. Encoding happens outside the lock# — pydantic dump and dict walk are CPU-bound and# thread-safe on a frozen model, so holding the lock across# the encode step would serialise work that does not need# mutual exclusion.from..redactionimportredact_structuredredacted=redact_structured(event.model_dump(mode="json"),rules=diagnostic_rules())line=json.dumps(redacted,sort_keys=True,separators=(",",":"))+"\n"withself._lock:handle=self._open()handle.write(line)handle.flush()exceptException:# Stdlib logging.Handler.emit contract: any emit-side failure# must route through handleError(record) so the application is# never killed by a logging path. Broad catch is mandated by# the cpython logging module's documented protocol.logger.warning("jsonl run sink emit failed",exc_info=True)self.handleError(record)
def_open(self)->TextIO:r"""Lazily open the JSONL file in append mode. ``newline=""`` disables the Python text-mode newline translation (CRLF on Windows) so ``events.jsonl`` stays byte-stable across platforms — emitting exactly one ``\\n`` per record on every OS. """ifself._handleisNone:self._handle=self._target.open("a",encoding="utf-8",newline="")returnself._handle
[docs]@overridedefclose(self)->None:"""Flush, :func:`os.fsync`, and close the underlying file handle. Always invokes the base :meth:`logging.Handler.close` so the handler is removed from the logging registry even when the flush or fsync raises. """try:withself._lock:handle=self._handleifhandleisnotNone:try:handle.flush()os.fsync(handle.fileno())finally:handle.close()self._handle=Nonefinally:super().close()