Skip to content

iter-24: hoppy container logs (syslog receiver + bore tunnel)#29

Merged
ractive merged 4 commits into
mainfrom
iter-24/container-logs-tunnel
May 9, 2026
Merged

iter-24: hoppy container logs (syslog receiver + bore tunnel)#29
ractive merged 4 commits into
mainfrom
iter-24/container-logs-tunnel

Conversation

@ractive

@ractive ractive commented May 9, 2026

Copy link
Copy Markdown
Owner

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:

hoppy container logs --app-id <id>
  • New bunny-syslog-receiver crate — tokio TCP listener with RFC 6587 octet-counted + non-transparent LF framing (auto-detected per connection), RFC 5424 / 3164 parsing via syslog_loose, mpsc-based event channel, CancellationToken shutdown.
  • Tunnel trait + three implsBoreTunnel (default; spawns the bore CLI, parses its listening at host:port banner), NoopTunnel (--tunnel none), StaticTunnel (--tunnel-host host:port for BYO tunnels like ssh -R).
  • CLI lifecycle — validate app, refuse-or-replace stale forwarding config (--replace-existing restores 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).
  • Docs — README "Tailing Magic Containers logs" section, bunny-api-quirks.md note, research/syslog-tunnel-options.md (bore vs. frp / rathole / ngrok / ssh -R / Cloudflare Tunnel / Tailscale Funnel), decision-log entry on the bunny-syslog-receiver crate name.

bore is not bundled — installed separately via cargo install bore-cli or brew install bore-cli. Users on corporate VPNs or with their own ingress can skip bore entirely with --tunnel none or --tunnel-host.

Test plan

  • cargo fmt --check clean
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo test --workspace --quiet — all passing (20 new unit tests in bunny-syslog-receiver, snapshot test for --help, full e2e suite)
  • Framing: octet-counted + LF-framed parsing, malformed-first-byte rejection, oversize-length rejection, clean-EOF handling
  • Tunnel: parse_bore_banner for old/new bore log formats, BoreTunnel install-hint error, NoopTunnel / StaticTunnel round-trips
  • Receiver: end-to-end TCP tests with both framing styles, malformed-connection isolation
  • Live forwarding round-trip — deliberately not in CI (would need a public ingress); covered by mock tests + iter-21 log-forwarding e2e
  • Manual dogfood against a real Bunny app (post-merge)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added hoppy container logs command for streaming live syslog output from Magic Containers applications
    • Supports multiple tunnel modes: bore (default), static host via --tunnel-host, or none
    • Output format options: text or json (NDJSON format)
  • Documentation

    • README updated with container logs workflow and troubleshooting guide
    • Privacy notice about bore.pub relay usage when using default tunnel mode
    • Output format guidance and delivery delay information

Review Change Stack

…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>
@ractive
ractive requested a review from Copilot May 9, 2026 10:43
@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@ractive has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 51 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Free

Run ID: 9f6dcf11-c0bb-4223-b496-f66fb3ffc2d3

📥 Commits

Reviewing files that changed from the base of the PR and between c2d5514 and 54ebb0b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • crates/bunny-syslog-receiver/Cargo.toml
  • crates/bunny-syslog-receiver/src/framing.rs
  • crates/bunny-syslog-receiver/src/receiver.rs
  • crates/bunny-syslog-receiver/src/tunnel.rs
  • hoppy-knowledgebase/iterations/iteration-24-container-logs-tunnel.md
  • hoppy-knowledgebase/iterations/iteration-25-publish.md
  • src/commands/container.rs
📝 Walkthrough

Walkthrough

This 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.

Changes

Container Logs Streaming Feature

Layer / File(s) Summary
Workspace & Library Setup
Cargo.toml, crates/bunny-syslog-receiver/Cargo.toml
Adds bunny-syslog-receiver as workspace member and dependency with workspace-managed versions; updates tokio workspace dependency to include signal feature alongside fs.
Library Module Structure
crates/bunny-syslog-receiver/src/lib.rs
Declares public modules (framing, receiver, tunnel) and re-exports core types (LogEvent, Severity, Tunnel, TunnelHandle).
Syslog Framing Protocol
crates/bunny-syslog-receiver/src/framing.rs
Implements frame detection (octet-counted vs LF-terminated), detect_framing() to select mode from first byte, and read_frame() to extract payloads with size/format validation and CRLF tolerance.
Receiver & Log Events
crates/bunny-syslog-receiver/src/receiver.rs
Defines Severity enum, LogEvent struct with RFC3164/5424 parsing, LocalListener socket wrapper, run_receiver() accept loop, and spawn_receiver() background runner with per-connection framing detection and message parsing.
Tunnel Abstraction
crates/bunny-syslog-receiver/src/tunnel.rs
Provides Tunnel trait with three implementations: BoreTunnel (spawns bore subprocess, waits for banner, manages child lifecycle), NoopTunnel (no-op identity tunnel), and StaticTunnel (user-provided host:port); includes banner parsing helpers.
CLI Argument Schema
src/cli.rs
Adds ContainerAction::Logs variant with options for tunnel mode (--tunnel/--tunnel-host), local port, output format, replace-existing flag, and bore server override.
Command Handler
src/commands/container.rs
Implements handle_logs() to validate format, check app existence, optionally replace Bunny log forwarding, bind listener, start tunnel, spawn receiver, stream and format LogEvents until exit, then clean up receiver, restore forwarding, and stop tunnel.
Tests & Documentation
tests/e2e/cli_container.rs, README.md, hoppy-knowledgebase/*
CLI help snapshot test, user-facing README guide with bore setup and troubleshooting, research notes on tunnel options (bore/frp/rathole/ngrok/ssh/cloudflare/tailscale), decision log entry for crate naming, and API quirks documentation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hops into logs with a syslog embrace,
Frames dance octet-counted through TCP space,
Bore tunnels light up, from receiver to cloud,
Events stream forward, by containers proud,
Watch them flow freely—no fetch, just reflect! 🌊


Note

🎁 Summarized by CodeRabbit Free

Your 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 @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-receiver crate (RFC 6587 framing detection + syslog parsing + tunnel abstraction).
  • Adds container logs CLI 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.

Comment thread src/commands/container.rs Outdated
if !replace_existing {
bail!(
"app {app_id} already has a log-forwarding config \
(endpoint={}). Run `hoppy log-forwarding delete \
Comment thread src/commands/container.rs Outdated
Comment on lines +2195 to +2197
let listener = LocalListener::bind(local_port)
.await
.context("failed to bind local syslog listener")?;
Comment on lines +161 to +180
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
Comment on lines +9 to +19
[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"
Comment on lines +70 to +72
// 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.",
ractive and others added 3 commits May 9, 2026 12:55
- 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>
@ractive
ractive merged commit 5b4d9ea into main May 9, 2026
4 of 7 checks passed
@ractive
ractive deleted the iter-24/container-logs-tunnel branch May 9, 2026 10:59
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.

2 participants