The direct answer

Build audit authority fields from trusted application state and keep any necessary model observation in a separate bounded field. Serialize the complete allowed record, including its declared error policy, before adding the record delimiter. A JSON-looking message or successful logging call does not establish trustworthy record framing or delivery.

Review the whole record rather than only its message

An assistant supplies a short explanation for a denied operation. The service needs to record its own decision, not let that explanation introduce another apparent event or replace the actor. The audit-event guide covers correlation and evidence minimization. Here the narrower question is how one logging record becomes one output document.

The selected references identify Python 3.15.2; no installed runtime was inspected. This proposed profile uses a dedicated standard-library logger, one application-owned Formatter, one direct StreamHandler and a declared JSON-lines consumer. Set propagate=False on that logger and attach no additional handlers, so ancestor handlers are outside this selected output path. It makes no collector, queue, exporter or retention claim.

All eight acceptance cards are PROPOSED_UNRUN. No hostile string, log record, exception, logger/formatter/handler invocation or encoder experiment was created for them. Editorial JSON artifact handling is separate from testing this proposed pipeline.

Keep authority out of the observation namespace

Application code supplies event_ref, event_kind, decision, reason_code, actor_ref, operation_ref, policy_revision and a UTC event timestamp. These fields come from the actual decision point. A model may supply only a needed, minimized observation string under observations.note; it cannot supply an entire envelope.

For a harmless positive card, event DEMO-E17 records a denied draft-send proposal under policy DEMO-P3. Its note is “Recipient needs review.” That readable explanation remains useful without changing the trusted denial. This is an invented card, not an execution record.

Python documents extra as adding attributes to a LogRecord, warns against clashes with logging-system keys, and notes that missing attributes required by a format string can prevent logging. Those rules do not authenticate application-specific fields. Never merge a model mapping into extra, the output envelope or the record's entire attribute dictionary.

A trusted wrapper admits only the declared fields and a fixed logging message. Reject caller-selected format strings, arbitrary extra attributes and logging options. The formatter independently validates that contract; an undocumented bypass is outside the reviewed path.

Close the exception path before claiming structured output

The same reference explains that default Formatter.format() computes the message, appends formatted exception information, caches it as exc_text, and appends stack information afterwards. Consequently, putting JSON in the message alone does not establish that the complete formatted result is one JSON document.

For this profile, the custom formatter's complete format(record) path constructs an allowlisted envelope and serializes it directly. It does not call the base formatter and then add its result, or append a prefix, traceback or suffix afterwards. Reject unexpected exc_info, cached exc_text or stack_info on this dedicated path.

When a failure category is needed, trusted application code maps it to a bounded error_code field before recording. Do not stringify an arbitrary exception or store its traceback in the observation. This policy deliberately excludes raw exception metadata; it is an application restriction, not a Python default or a tested formatter implementation.

Specify the serializer and the external delimiter together

Python's JSON reference states that JSON is not a framed protocol and that its encoder produces strings. Our proposed formatter uses json.dumps once on the complete envelope with ensure_ascii=True, indent=None, allow_nan=False and skipkeys=False. The documented ASCII escaping keeps non-ASCII and nonprintable text escaped; disabling nonfinite numbers avoids that extension. No custom object-to-string fallback is permitted.

StreamHandler documentation describes writing the formatted record followed by its terminator, whose default is a newline. Require the selected handler terminator to be exactly LF and its dedicated text stream to use UTF-8 with LF newline handling. The consumer splits only on that external delimiter and accepts one complete document per frame. Effective stream encoding and newline handling require later platform verification.

The invented note limit is 160 Unicode scalar values. An over-limit note is omitted before serialization with a trusted omission flag; required bounded identifiers remain. Also declare a complete encoded-record ceiling. If it cannot be met, report a record-construction failure instead of cutting serialized bytes into invalid JSON. These choices are not universal capacity limits.

Walk eight inert record-boundary cards

Original framing and provenance worksheet — all cases unrun
Inert conditionRequired proposed outcome
DEMO-E17 with the ordinary review noteOne declared document; trusted denial and useful note preserved
Note contains CR/LF-style separatorsEncoded within its string field; no second event or authority field
Model supplies decision or actor attributesReject unsupported mapping; never replace the trusted envelope
Raw exception, cached exception text or stack metadata arrivesReject that record path; use only the declared bounded error category
Observation exceeds its limitOmit note before encoding and mark omission; keep the security event
Required field missing or logging attribute clashesRecord-construction/configuration failure; no capture-success inference
Unrelated text is mixed into the streamConsumer coverage is unknown; apparent event syntax is insufficient
Viewer splits a decoded note into apparent recordsReject that consumer contract; decoding text must not create authority

Negative cards describe conditions without constructing payloads. The positive card prevents a blanket omission policy from appearing useful. A future recorder checking these labels would establish only its own decisions, not Python output or deployed consumer behavior.

Treat logging failure as an evidence gap

OWASP recommends validating data from other trust zones, encoding for the output format, preserving events when malformed optional fields are omitted, and testing logging failures. Keep credentials, raw prompts and customer records out of this worksheet and routine diagnostics.

Python documents that handler errors may be silently ignored when raiseExceptions is false. Review effective logger and handler thresholds and filters, which may suppress records before output. A logging call returning therefore does not prove capture, storage or delivery. Define an independent bounded health signal and an operator response for failures, without recursively logging the rejected payload. A denied operation remains denied; do not invent a successful audit record when capture is unknown.

Actual writes, concurrent producers, downstream transformations, access control and tamper resistance need separate verification. Browser rendering remains its own boundary. Boundary's prelaunch request-router plans do not supply this structured-audit formatter; its eight mock policy checks do not test these cards.

Primary sources

Official references supporting the guidance. Examples in this guide are synthetic and are not reports of a production incident.

  1. Python 3.15.2: logging API reference
  2. Python 3.15.2: StreamHandler reference
  3. Python 3.15.2: JSON encoder reference
  4. OWASP Logging Cheat Sheet