Skip to content

feat: rotation-aware log streaming engine + sdk bindings#755

Open
toksdotdev wants to merge 9 commits into
mainfrom
feat/log-stream
Open

feat: rotation-aware log streaming engine + sdk bindings#755
toksdotdev wants to merge 9 commits into
mainfrom
feat/log-stream

Conversation

@toksdotdev
Copy link
Copy Markdown
Member

summary

  • new microsandbox::logs engine: log_stream (Stream<LogEntry> with cursor-based resume and notify-driven follow) plus read_logs (sorted snapshot, drains the same engine), covering exec.log and rotated siblings, runtime.log, and kernel.log. introduced MicrosandboxError::MissedRotation for consumers that fall behind retention, and InvalidCursor for bad resume tokens.
  • msb logs and msb logs -f now drive the engine instead of a bespoke poll loop. snapshot uses read_logs (chronologically sorted); --follow uses log_stream with start: Since(now) after the snapshot completes and surfaces MissedRotation with a hint to restart.
  • node, python, and go sdks gained logStream / log_stream / LogStream wrappers on both Sandbox and SandboxHandle. all three mirror the existing metrics-stream pattern (async iterator on node/python, Recv/Close handle on go). every LogEntry carries an opaque cursor field (snapshot + stream) for resume. since / fromCursor are mutually exclusive; passing both rejects at the binding boundary.
  • minor: renamed the internal Multiplexer to LogEngine and folded the cli's mask_to_engine_sources into a SourceMask.to_engine_sources() method.

test plan

  • cargo test -p microsandbox --lib logs:: (34 unit tests across cursor / parser / stream / types)
  • cargo test -p microsandbox --test log_stream (8 integration tests driving the public api through synthetic on-disk layouts via MSB_HOME tempdir)
  • cargo test -p microsandbox-cli --lib commands::logs:: (13 unit tests)
  • cargo check --workspace + cargo clippy --workspace clean
  • node sdk: npm run build:native && npm run build:ts && npm run typecheck && npm run test:unit (64 unit tests)
  • python sdk: maturin develop then pytest tests/
  • go sdk: go test ./sdk/go/... (4 new bridge tests around Cursor field + LogStreamOptions)
  • manual: MSB_HOME=$tmpdir msb logs <name> with hand-laid exec.log exercises the snapshot path; appending entries with future timestamps validates -f picks them up via fs notify

- added `microsandbox::logs` module owning `log_stream` (returns a `Stream<LogEntry>`) and `read_logs` (sorted snapshot) over `exec.log` plus rotated siblings, `runtime.log`, and `kernel.log`.
- `log_stream` supports cursor-based per-source resume and an optional `follow` mode driven by filesystem notify events with a poll fallback.
- opaque `LogCursor` snapshots positions across active sources at emit time; pass it to `LogStreamStart::From` to resume.
- introduced `MicrosandboxError::MissedRotation` for consumers that fall behind the on-disk retention window and `InvalidCursor` for malformed resume tokens.
- removed the prior polling `sandbox/logs.rs`; callers (CLI, rust example, python/node/go sdks) now reach the api via `microsandbox::logs::*` and await the now-async `read_logs`.
- added integration tests that drive the public api against synthetic on-disk log layouts via an `MSB_HOME` tempdir, plus engine-level unit tests for the parser, cursor, and multiplexer.
- replaced the bespoke poll loop in `msb logs` with `microsandbox::logs`: snapshots go through `read_logs` (chronologically sorted) and `--follow` consumes a `log_stream` with `start: Since(now)` after the snapshot completes.
- dropped the local rotation tracking, JSON Lines reader, text-log timestamp parser, and the 200ms `tokio::time::sleep` poll; `--follow` now wakes on filesystem change notifications via the engine.
- `--follow` surfaces `MicrosandboxError::MissedRotation` with a hint to restart, replacing the prior silent gap when retention outpaced the consumer.
- new bridge helpers `mask_to_engine_sources` and `engine_entry_to_cli` translate the cli source flags into engine sources and re-encode non-utf8 bodies as base64 so `--json` keeps round-trip fidelity.
…sources onto SourceMask

- renamed `Multiplexer` to `LogEngine` across `crates/microsandbox/lib/logs/{mod,stream}.rs` and the engine module's docstring; the name is more descriptive of what the type does and matches the way the rest of the codebase talks about subsystem engines.
- moved the cli's free `mask_to_engine_sources(mask)` into a `SourceMask::to_engine_sources(self)` method so the conversion lives next to the data it operates on; updated the single caller and the unit test.
- exposed `Sandbox.logStream(opts)` and `SandboxHandle.logStream(opts)`. both return a `LogStream`: an `AsyncIterable<LogEntry>` (`for await...of` friendly) with a `recv()` method and `Symbol.asyncDispose` for cleanup, mirroring `MetricsStream`.
- added `LogStreamOptions` with `sources`, `since`, `fromCursor`, `until`, and `follow`. `since` and `fromCursor` are mutually exclusive; passing both rejects at the napi boundary.
- added an opaque `cursor: string` field on every `LogEntry` (snapshot and streaming paths). pass it back via `fromCursor` to resume strictly after that entry.
- native side: new `JsLogStream` class wraps a `tokio::mpsc::Receiver<Result<LogEntry>>` fed by a background task that drains the engine stream; shared helper `spawn_log_stream` is reused by both `Sandbox` and `SandboxHandle`.
- exposed `Sandbox.log_stream(...)` and `SandboxHandle.log_stream(...)`. both return a `LogStream`: an async iterator (`async for entry in stream`) of `LogEntry` values that calls back into the engine via `pyo3_async_runtimes::tokio::future_into_py`, mirroring `MetricsStream`.
- added keyword args `sources`, `since_ms`, `from_cursor`, `until_ms`, `follow`. `since_ms` and `from_cursor` are mutually exclusive; passing both raises `ValueError` at the binding boundary.
- added an opaque `cursor: str` field on every `LogEntry` (snapshot and streaming paths). pass it back via `from_cursor` to resume strictly after that entry.
- exported `LogEntry` and `LogStream` from the top-level `microsandbox` package — `LogEntry` was previously available only via the private `_microsandbox` module.
- exposed `Sandbox.LogStream(ctx, opts)` and `SandboxHandle.LogStream(ctx, opts)`. both return a `*LogStreamHandle` with `Recv(ctx)` (returns the next entry, or `nil, nil` when the stream ends) and `Close()`, mirroring `MetricsStreamHandle`.
- added `LogStreamOptions` with `Sources`, `Since`, `FromCursor`, `Until`, `Follow`. `Since` and `FromCursor` are mutually exclusive; passing both rejects at the ffi boundary.
- added a `Cursor` field on `LogEntry` (snapshot + streaming paths). pass it back via `FromCursor` to resume strictly after that entry.
- native side: new ffi entry points `msb_sandbox_log_stream`, `msb_sandbox_handle_log_stream`, `msb_log_recv`, `msb_log_close`. background forwarder drains the engine stream into an unbounded `mpsc` channel; the go side blocks on `recv()` per call.
annotated `RotationConfig::{path, find_index, queue_all_rotated}`, `LogEngine::{new, snapshot_cursor, step, build_watcher, into_stream}`, and `Reader::{open, read_chunk, handle_rotation, replace_live}` plus the helper `StepResult` enum and `ReaderState` struct. one-to-three-sentence comments describing intent and side effects.
new `tests/log_stream_e2e.rs` boots an alpine sandbox under `#[msb_test]` and exercises the relay-tap → exec.log → consumer path end-to-end:

- `logs_captures_exec_stdout_from_running_sandbox`: snapshot via `Sandbox::logs` after an exec; asserts the marker arrives with `source = Stdout` and a real `session_id`.
- `log_stream_follow_catches_live_writes`: opens `log_stream(follow: true, start: Since(now))` first, then execs; asserts the marker arrives on the live stream within 5s (validating the notify-driven wake path against a real producer).
- `log_stream_resume_from_cursor_excludes_replayed_entries`: execs A, captures the cursor of A's entry, execs B, opens a fresh stream from that cursor; asserts B is yielded but A is not replayed.

each scenario uses a unique sandbox name and cleans up on success. runs only via `cargo nextest run -p microsandbox --test log_stream_e2e --run-ignored=only` (msb_test marks them ignored by default).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant