Skip to content

Preserve recent node logs and fan out log streams per subscriber#55

Open
W0lfD wants to merge 1 commit into
PasarGuard:mainfrom
W0lfD:preserve-logs
Open

Preserve recent node logs and fan out log streams per subscriber#55
W0lfD wants to merge 1 commit into
PasarGuard:mainfrom
W0lfD:preserve-logs

Conversation

@W0lfD
Copy link
Copy Markdown

@W0lfD W0lfD commented Jun 4, 2026

Summary

This PR changes node log streaming from a live-only/shared-consumer model to a bounded fan-out log stream.

The node now keeps the latest log lines in an in-memory ring buffer. When a Logs client subscribes, it receives the buffered recent lines first and then continues receiving live updates. Each subscriber has its own stream, so multiple open panels no longer compete for log lines.

Problem

Before this change, the Logs page only showed lines received while the current stream was connected. This caused a few confusing and inconvenient behaviors:

  • Refreshing the Logs page lost all previously displayed lines
  • If no new Xray log was produced, the page could look like it was still loading or disconnected
  • Temporary panel disconnects during proxy/network testing caused missed log lines
  • Multiple open Logs pages could compete for lines from the same stream

This made the logs less useful exactly during the setup/debugging flow where they are most needed

Changes

  • Added a reusable bounded logstream.Buffer
  • Store recent node log lines in memory
  • Changed backend log access to a subscription model
  • New subscribers receive a snapshot of recent buffered lines before live updates
  • Fan out live log lines to all active subscribers
  • Updated REST and gRPC log streaming to use independent subscriptions
  • Added/updated tests for buffer behavior and log streaming behavior

Validation performed:

  • make test
  • manual testing with a fully running panel + node setup

Summary by CodeRabbit

  • Refactor
    • Log streaming API updated: Logs() method replaced with context-aware SubscribeLogs() for improved cancellation handling
    • Enhanced multi-subscriber log buffering for better resource management during concurrent log access
    • Log resources now properly cleaned up during service shutdown

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

Walkthrough

This PR refactors backend log streaming from simple unbuffered channels to a context-aware buffered subscription model. A new logstream.Buffer provides concurrent ring-buffering with multi-subscriber fan-out, replay of recent log lines, and context-driven lifecycle management. The Backend interface method signature changes from Logs() to SubscribeLogs(context.Context), and WireGuard, XRay, and controller implementations are updated accordingly.

Changes

Log Subscription Refactoring

Layer / File(s) Summary
Interface contract and Buffer abstraction
backend/backend.go, backend/logstream/buffer.go, backend/logstream/buffer_test.go
Backend interface replaces Logs() with SubscribeLogs(context.Context). New logstream.Buffer struct maintains a fixed-size ring buffer with mutex-protected access, publishes log lines to all registered subscribers via non-blocking sends, subscribers receive a snapshot of buffered lines upon subscribe and then live lines, and context cancellation or buffer close triggers channel closure and cleanup. Three comprehensive tests verify tail replay across multiple subscribers, non-blocking behavior on slow subscribers, and proper closure semantics.
WireGuard log refactoring
backend/wireguard/wireguard.go, backend/wireguard/log.go, backend/wireguard/wireguard_lifecycle_test.go
WireGuard struct field changes from logChan chan string to logs *logstream.Buffer. Initialization creates the buffer from configuration size. New SubscribeLogs(ctx) method returns subscriptions via buffer or a closed channel if nil. emitLogLocked publishes lines via logs.Publish() instead of non-blocking channel send. Shutdown closes and nils the buffer. Lifecycle tests are updated to subscribe via SubscribeLogs and verify startup/shutdown log messages and buffer cleanup.
XRay log refactoring
backend/xray/core.go, backend/xray/log.go, backend/xray/xray.go, backend/xray/xray_test.go
XRay Core struct field changes from logsChan to logs *logstream.Buffer, initialized in NewXRayCore. New SubscribeLogs(ctx) and CloseLogs() methods expose subscription and cleanup. Log capture functions captureStartupLogLine and captureRuntimeLogLine publish to buffer via logs.Publish(). Xray wrapper adds SubscribeLogs(ctx) that safely delegates to core's subscription, returning a closed channel when core is nil, and shutdown calls CloseLogs(). Test is updated to use the new subscription API.
Controller integration
controller/rest/log.go, controller/rpc/log.go
REST and RPC log handlers call Backend().SubscribeLogs(r.Context()) and Backend().SubscribeLogs(stream.Context()) respectively, binding log stream subscriptions to request/RPC lifetimes instead of using a static backend channel.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Logs now flow through buffered streams so fine,
Context-bound subscriptions keep them in line,
Ring buffers replay the tail so dear,
Multiple subscribers need not fear!
WireGuard and XRay both sing their tune,
Request contexts close them just in tune. 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introducing log preservation in a ring buffer and converting to a fan-out subscription model instead of a shared channel.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
controller/rpc/log.go (1)

15-18: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat closed log subscription as graceful stream completion.

Line 17 returns an error when logChan closes, but with SubscribeLogs(stream.Context()) channel closure is expected on normal cancellation and backend log buffer shutdown. This can turn valid stream termination into client-visible gRPC errors.

Suggested fix
 import (
-	"errors"
 	"fmt"

 	"github.com/pasarguard/node/common"
 )
@@
 		case log, ok := <-logChan:
 			if !ok {
-				return errors.New("log channel closed")
+				return nil
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/rpc/log.go` around lines 15 - 18, The current handler for logs
treats a closed logChan as an error; when SubscribeLogs(stream.Context()) closes
the channel on normal cancellation or backend shutdown the code in the loop
(checking case log, ok := <-logChan) should treat ok==false as graceful
completion instead of returning an error—modify the branch to exit the
function/loop and return nil (or otherwise signal successful completion) when
logChan is closed so normal stream cancellation does not produce a gRPC error
from this code path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@controller/rpc/log.go`:
- Around line 15-18: The current handler for logs treats a closed logChan as an
error; when SubscribeLogs(stream.Context()) closes the channel on normal
cancellation or backend shutdown the code in the loop (checking case log, ok :=
<-logChan) should treat ok==false as graceful completion instead of returning an
error—modify the branch to exit the function/loop and return nil (or otherwise
signal successful completion) when logChan is closed so normal stream
cancellation does not produce a gRPC error from this code path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c74e8c9a-736b-4c82-9bad-4c0dae78e72e

📥 Commits

Reviewing files that changed from the base of the PR and between 24428aa and 0813b4c.

📒 Files selected for processing (12)
  • backend/backend.go
  • backend/logstream/buffer.go
  • backend/logstream/buffer_test.go
  • backend/wireguard/log.go
  • backend/wireguard/wireguard.go
  • backend/wireguard/wireguard_lifecycle_test.go
  • backend/xray/core.go
  • backend/xray/log.go
  • backend/xray/xray.go
  • backend/xray/xray_test.go
  • controller/rest/log.go
  • controller/rpc/log.go

@M03ED
Copy link
Copy Markdown
Contributor

M03ED commented Jun 4, 2026

this should be in pkg directory not backend, this is not a backend

@M03ED
Copy link
Copy Markdown
Contributor

M03ED commented Jun 4, 2026

backend/logstream/buffer.go creates each subscriber channel with capacity equal to the replay buffer size, then fills it completely with the snapshot before registering the subscriber. If the buffer is full, the new subscriber starts with a full channel. Any Publish that happens before the REST/RPC handler drains at least one replayed line will hit the nonblocking default path and drop that live log for this newly connected client. For a “replay tail, then stream live logs” API, that creates a connection-time loss window.

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