Skip to content

output_capture

output_capture

Tee a block's stdout/stderr to a durable sink, live terminal untouched.

tee_output wraps an operation (an image build, a task launch — anything that spawns subprocesses) and copies every byte those subprocesses write to stdout/stderr into a durable sink, without disturbing the live terminal:

  • a pseudo-terminal fronts the wrapped block when stdout is a TTY, so a child like podman still sees isatty(1) == True and keeps its colour + progress output — the forwarded bytes are byte-for-byte what the operator would have seen;
  • a plain pipe is used when stdout is not a TTY (redirected / CI), matching ordinary non-interactive behaviour while still capturing the stream.

The sink follows the same journald-else-file rule as the rest of terok logging: captured output goes to journald (line-buffered into structured entries via JournalWriter) when its socket is present, else to a caller-provided file resolved lazily. The module stays generic — callers pass the journald fields and a file_path_fn; nothing here knows a project or a state-dir layout.

__all__ = ['tee_output'] module-attribute

tee_output(identifier, *, fields=None, file_path_fn=None)

Capture the wrapped operation's output to journald or a log file.

Picks a journald sink when its socket is present, else a file at file_path_fn() (resolved lazily, so no directory is created on the journald path). With neither available the block still runs, un-teed — durability is a bonus that never blocks the operation.

Parameters:

Name Type Description Default
identifier str

SYSLOG_IDENTIFIER for journald entries.

required
fields dict[str, str] | None

Static journald fields (e.g. {"TEROK_KIND": "build"}); also drive the journalctl hint.

None
file_path_fn Callable[[], Path] | None

Zero-arg callable returning the fallback log path.

None
Source code in src/terok_util/output_capture.py
@contextlib.contextmanager
def tee_output(
    identifier: str,
    *,
    fields: dict[str, str] | None = None,
    file_path_fn: Callable[[], Path] | None = None,
) -> Iterator[None]:
    """Capture the wrapped operation's output to journald or a log file.

    Picks a journald sink when its socket is present, else a file at
    ``file_path_fn()`` (resolved lazily, so no directory is created on the
    journald path).  With neither available the block still runs, un-teed —
    durability is a bonus that never blocks the operation.

    Args:
        identifier: ``SYSLOG_IDENTIFIER`` for journald entries.
        fields: Static journald fields (e.g. ``{"TEROK_KIND": "build"}``);
            also drive the ``journalctl`` hint.
        file_path_fn: Zero-arg callable returning the fallback log path.
    """
    fields = fields or {}
    sink: _StreamSink
    if journald_available():
        sink = _JournalStreamSink(identifier, fields)
    elif file_path_fn is not None:
        try:
            sink = _FileStreamSink(file_path_fn())
        except OSError:
            yield
            return
    else:
        yield
        return
    try:
        with _capture(sink):
            yield
    finally:
        with contextlib.suppress(Exception):
            sink.close()
        print(f"↳ {sink.hint()}", file=sys.stderr)