iter-24: hoppy container logs (syslog receiver + bore tunnel)#29
Conversation
…l (iter-24)
Bunny Magic Containers has no logs-fetch API — logs are syslog-forwarded
only. This iteration collapses the previous manual workflow (provision a
public syslog endpoint → register forwarding → tail → remember to delete)
into a single foreground command:
hoppy container logs --app-id <id>
* New `bunny-syslog-receiver` crate: tokio TCP listener with RFC 6587
octet-counted + non-transparent LF framing detection, RFC 5424 / 3164
parsing via `syslog_loose`, mpsc-based event channel, CancellationToken
shutdown.
* `Tunnel` trait + `BoreTunnel` (spawns the `bore` CLI), `NoopTunnel`
(`--tunnel none`), `StaticTunnel` (`--tunnel-host host:port` for BYO
tunnels like `ssh -R`).
* CLI lifecycle: validate app, refuse-or-replace stale forwarding config,
start receiver, start tunnel, register forwarding, stream events with
severity-coloured pretty-printing or NDJSON, tear everything down on
Ctrl-C (forwarding-delete first, then tunnel).
* `--format text` (default, colour) / `--format json` (NDJSON);
`--format table` is rejected with a friendly error.
* Docs: README "Tailing Magic Containers logs" section, quirks note,
tunnel-tool research note, decision-log entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Free Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThis PR adds live log streaming for Magic Containers through a new embedded syslog receiver crate. It implements RFC 3164/5424 frame parsing, multiple tunnel providers (bore/static/noop), CLI integration, and command handler logic that orchestrates listener binding, log forwarding configuration, and event streaming with cleanup. ChangesContainer Logs Streaming Feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login. Comment |
There was a problem hiding this comment.
Pull request overview
Adds a new hoppy container logs foreground command to stream live Magic Containers logs by standing up a local TCP syslog receiver, optionally exposing it via a tunnel (default: bore), temporarily configuring Bunny log forwarding, and tearing everything down on exit/Ctrl‑C. This fits into the CLI’s Magic Containers workflow by making log tailing a single command despite Bunny’s syslog-only logging model.
Changes:
- Introduces the
bunny-syslog-receivercrate (RFC 6587 framing detection + syslog parsing + tunnel abstraction). - Adds
container logsCLI surface + command handler to orchestrate receiver/tunnel/forwarding lifecycle and stream output as text or NDJSON. - Adds docs + e2e help snapshot coverage for the new command.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/e2e/snapshots/e2e__cli_container__container_logs_help.snap | Snapshot for hoppy container logs --help output. |
| tests/e2e/cli_container.rs | Adds an e2e test that asserts the container logs help snapshot. |
| src/commands/container.rs | Implements ContainerAction::Logs handler: receiver + tunnel startup, forwarding create/delete, and streaming output. |
| src/cli.rs | Adds the container logs subcommand and flags (--tunnel, --tunnel-host, --format, etc.). |
| README.md | Documents how to tail Magic Containers logs and how to install/avoid bore. |
| hoppy-knowledgebase/research/syslog-tunnel-options.md | Research note comparing tunnel options and rationale for defaulting to bore. |
| hoppy-knowledgebase/iterations/iteration-24-container-logs-tunnel.md | Updates iteration status metadata. |
| hoppy-knowledgebase/decision-log.md | Records naming decision for bunny-syslog-receiver. |
| hoppy-knowledgebase/api/bunny-api-quirks.md | Documents the “no logs-fetch endpoint” quirk and the syslog-forwarding requirement. |
| crates/bunny-syslog-receiver/src/tunnel.rs | Implements Tunnel trait with BoreTunnel, NoopTunnel, and StaticTunnel. |
| crates/bunny-syslog-receiver/src/receiver.rs | Tokio TCP syslog receiver producing structured LogEvents over an mpsc channel. |
| crates/bunny-syslog-receiver/src/lib.rs | Crate module wiring + public re-exports for receiver/tunnel components. |
| crates/bunny-syslog-receiver/src/framing.rs | RFC 6587 octet-counted vs LF framing detection and frame extraction. |
| crates/bunny-syslog-receiver/Cargo.toml | Declares the new crate and its dependencies. |
| Cargo.toml | Adds the new crate to the workspace and to hoppy dependencies; enables tokio signal for Ctrl‑C handling. |
| Cargo.lock | Locks new direct/transitive dependencies for syslog parsing and tunneling support. |
| if !replace_existing { | ||
| bail!( | ||
| "app {app_id} already has a log-forwarding config \ | ||
| (endpoint={}). Run `hoppy log-forwarding delete \ |
| let listener = LocalListener::bind(local_port) | ||
| .await | ||
| .context("failed to bind local syslog listener")?; |
| let mut buf = Vec::with_capacity(512); | ||
| let n = reader.read_until(b'\n', &mut buf).await?; | ||
| if n == 0 { | ||
| return Ok(None); | ||
| } | ||
| if buf.len() > MAX_FRAME_BYTES { | ||
| return Err(FramingError::FrameTooLarge); | ||
| } | ||
| // Strip the trailing LF (and a CR if present, for CRLF tolerance). | ||
| if buf.last() == Some(&b'\n') { | ||
| buf.pop(); | ||
| } | ||
| if buf.last() == Some(&b'\r') { | ||
| buf.pop(); | ||
| } | ||
| if buf.is_empty() { | ||
| // A bare LF — skip and keep reading. | ||
| return Box::pin(read_lf_terminated(reader)).await; | ||
| } | ||
| Ok(Some(buf)) |
| //! | ||
| //! Bind a kernel-assigned port (or a caller-chosen one), accept connections, | ||
| //! detect framing on the first byte, and forward parsed [`LogEvent`]s on an | ||
| //! `mpsc` channel. Shutdown is cooperative: drop the |
| [dependencies] | ||
| anyhow.workspace = true | ||
| bytes.workspace = true | ||
| serde = { workspace = true, features = ["derive"] } | ||
| serde_json.workspace = true | ||
| tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net", "io-util", "sync", "process", "time"] } | ||
| tokio-util = { workspace = true, features = ["io"] } | ||
| syslog_loose = "0.23" | ||
| thiserror = "2" | ||
| async-trait = "0.1" | ||
| tracing = "0.1" |
| // We only depend on `thiserror` for these errors. Add it to Cargo.toml if | ||
| // not already present; otherwise switch to a hand-rolled `Display` impl. | ||
|
|
| anyhow!( | ||
| "`{}` not found on PATH. Install with `cargo install bore-cli` or \ | ||
| `brew install bore-cli`, or pass `--tunnel none` / \ | ||
| `--tunnel-host <host:port>` to skip the bundled tunnel.", |
- framing.rs: replace recursive Box::pin in read_lf_terminated with a loop to avoid unbounded recursion / stack overflow on a stream of bare LFs (CodeRabbit + Copilot). - receiver.rs (tests): unwrap server.await result so receiver-task panics surface in tests instead of being silently swallowed (CodeRabbit). - receiver.rs: clarify shutdown docs — call .cancel() on the CancellationToken; dropping it does not cancel (Copilot). - container.rs: fix bail message to point at the real CLI path `hoppy container log-forwarding delete` (Copilot). - container.rs: bind the local listener to 0.0.0.0 when --tunnel none and no --tunnel-host is given so the user's own ingress can reach it; keep 127.0.0.1 for bore / static-host where the tunnel terminates locally (Copilot). - Cargo.toml: drop unused `bytes` and `serde_json` deps (Copilot). - framing.rs: remove stale comment about thiserror dependency (Copilot). - tunnel.rs: reword install hint — bore is not bundled, say "skip bore" (Copilot). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mark scope checkboxes against the actual merged diff. - Receiver crate: 7/7 (all framing/parsing/shutdown/tests landed). - Tunnel abstraction: 5/5 (BoreTunnel + NoopTunnel + StaticTunnel + ngrok out-of-scope as designed). - `hoppy container logs` subcommand: 5/6 — `--redact` not wired up despite iter-21 having merged; panic-safe Drop guard not implemented. - Tests: 2/5 — fake-bore-binary test, mock CLI integration test, and output-format snapshot tests were not built. Only a `--help` snapshot was added. - Docs: 4/4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iter-24 added a 9th workspace crate (bunny-syslog-receiver) and an optional runtime dependency on the bore CLI. iter-25's publish checklist now reflects: - Cargo-search uniqueness includes bunny-syslog-receiver. - Crate publish order includes bunny-syslog-receiver between the bunny-api-* group and hoppy. - README polish notes bore as optional runtime dep, not a hard Homebrew dependency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Bunny Magic Containers has no logs-fetch API — logs are syslog-forwarded only. This iteration collapses the previous manual workflow (provision public syslog endpoint → register forwarding → tail → remember to delete) into a single foreground command:
bunny-syslog-receivercrate — tokio TCP listener with RFC 6587 octet-counted + non-transparent LF framing (auto-detected per connection), RFC 5424 / 3164 parsing viasyslog_loose, mpsc-based event channel,CancellationTokenshutdown.Tunneltrait + three impls —BoreTunnel(default; spawns theboreCLI, parses itslistening at host:portbanner),NoopTunnel(--tunnel none),StaticTunnel(--tunnel-host host:portfor BYO tunnels likessh -R).--replace-existingrestores the prior config on clean exit), start receiver, start tunnel, register forwarding, stream events with severity-coloured pretty-printing (default) or NDJSON (--format json), tear everything down on Ctrl-C (forwarding-delete first, then tunnel).bunny-api-quirks.mdnote,research/syslog-tunnel-options.md(bore vs. frp / rathole / ngrok / ssh -R / Cloudflare Tunnel / Tailscale Funnel), decision-log entry on thebunny-syslog-receivercrate name.bore is not bundled — installed separately via
cargo install bore-cliorbrew install bore-cli. Users on corporate VPNs or with their own ingress can skip bore entirely with--tunnel noneor--tunnel-host.Test plan
cargo fmt --checkcleancargo clippy --workspace --all-targets -- -D warningscleancargo test --workspace --quiet— all passing (20 new unit tests inbunny-syslog-receiver, snapshot test for--help, full e2e suite)parse_bore_bannerfor old/new bore log formats,BoreTunnelinstall-hint error,NoopTunnel/StaticTunnelround-trips🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
hoppy container logscommand for streaming live syslog output from Magic Containers applications--tunnel-host, or noneDocumentation