judgevet.adapters.outbound.audit_jsonl

Status: draft.

judgevet.adapters.outbound.audit_jsonl

Append each audit record to a local file as one JSON line.

JsonlAuditSink is the reference AuditSink. It writes one compact UTF-8 JSON object per JudgmentRecord, terminated by a newline, with no byte order mark. Source: https://jsonlines.org/. Source: https://github.com/ndjson/ndjson-spec. Keys follow the record's field order, and every field appears on every line. A None field is written as JSON null. The timestamp is RFC 3339 text from datetime.isoformat(), with a +00:00 offset. Source: https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md. Answers use the API wire shape. JSON object keys are strings, so a Score answer's legend and probabilities read back with string keys. The encoder refuses NaN and infinity, because neither is JSON. Source: https://docs.python.org/3/library/json.html.

The sink opens the file at construction with O_APPEND and mode 0o600. Each line reaches the file in one os.write call under a lock. On a local POSIX file, O_APPEND makes the seek and the write one atomic step. That guarantee does not hold on NFS with several appenders. Source: https://man7.org/linux/man-pages/man2/open.2.html. Rotation and retention belong to the caller.

Examples:

import tempfile
from datetime import UTC, datetime
from pathlib import Path

from judgevet.adapters.outbound.audit_jsonl import JsonlAuditSink
from judgevet.domain.audit import JudgmentRecord

record = JudgmentRecord(
    timestamp=datetime(2026, 9, 30, tzinfo=UTC),
    outcome="error",
    requested_model="jev-latest",
    questions={"q1": "noul"},
)
with tempfile.TemporaryDirectory() as folder:
    path = Path(folder) / "audit.jsonl"
    with JsonlAuditSink(path) as sink:
        sink.record(record)
    assert len(path.read_text(encoding="utf-8").splitlines()) == 1
See Also

JsonlAuditSink

Thread-safe append-only JSON Lines file sink for audit records.

The constructor opens the file, so a missing directory or a denied path raises there. A record error propagates to the caller; the HTTP adapters catch it and report audit_error on the http.call event. With fsync set, each write is flushed to storage before record returns. That blocks the event loop when the async adapter calls the sink. Source: https://docs.python.org/3/library/logging.handlers.html.

Attributes:

Name Type Description
path str

Filesystem path the sink appends to.

fsync bool

Whether each write is followed by os.fsync.

Examples:

import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as folder:
    sink = JsonlAuditSink(Path(folder) / "audit.jsonl", fsync=True)
    sink.close()
    assert sink.fsync

__enter__()

Return the open sink.

Returns:

Type Description
Self

This sink.

__exit__(exc_type, exc, traceback)

Close the sink on leaving the block.

Parameters:

Name Type Description Default
exc_type type[BaseException] | None

Exception class raised in the block, if any.

required
exc BaseException | None

Exception raised in the block, if any.

required
traceback TracebackType | None

Traceback of that exception, if any.

required

__init__(path, *, fsync=False)

Open the file for appending, creating it with mode 0o600.

Parameters:

Name Type Description Default
path str | PathLike[str]

File to append to. Its directory must exist.

required
fsync bool

Flush each line to storage before returning.

False

Raises:

Type Description
FileNotFoundError

If the parent directory does not exist.

OSError

If the file cannot be opened for appending.

close()

Close the file descriptor once; later calls do nothing.

record(record)

Append one record as one JSON line.

Parameters:

Name Type Description Default
record JudgmentRecord

The record to append.

required

Raises:

Type Description
ValueError

If the sink is closed, with the message audit sink is closed, or a number is not finite.

OSError

If the write or the flush fails.