How Xray-core Stats Channels Enable Real-Time Traffic Monitoring
Xray-core provides a generic, thread-safe stats channel—implemented in app/stats/channel.go—that broadcasts live traffic metrics via a publish-subscribe model, allowing external tools to monitor bytes transferred, connection counts, and latency in real time without blocking the proxy data path.
The stats channel is the backbone of traffic observability in the XTLS/Xray-core proxy framework. By decoupling metric generation from consumption through a lock-protected Go channel, Xray-core ensures that high-throughput traffic processing remains unaffected while observers receive live statistic streams.
Architecture of the Stats Channel System
The stats subsystem centers on the stats.Channel interface defined in the stats manager (app/stats/stats.go). The following components collaborate to deliver real-time metrics:
- Channel (
app/stats/channel.go): Holds a buffered Go channel (chan channelMessage), a slice of subscriber channels, and synchronization primitives to broadcast messages safely. - ChannelConfig (
app/stats/config.proto): Exposes tuning parameters includingblockingmode,subscriber_limit, andbuffer_size. - Manager (
app/stats/stats.go): Registers, starts, and shuts down channels viaRegisterChannel,GetChannel,Start, andClose. - Publishers: Components like inbound/outbound handlers that call
Channel.Publish(ctx, msg)whenever metrics are generated. - Subscribers: External tools (CLI, gRPC clients, REST APIs) that obtain a receive-only channel via
Channel.Subscribe()to consume messages as they arrive.
Channel Lifecycle
The lifecycle of a channel follows four distinct phases, each protected by mutexes to ensure thread safety under concurrent load.
1. Creation
NewChannel(&ChannelConfig{...}) allocates the internal message channel with the configured buffer size and stores the limit and blocking flags【channel.go‑L33】.
2. Start
c.Start() spawns a goroutine that continuously selects on:
- Incoming
channelMessagevalues (published byPublish) → broadcast to every subscriber (blocking or non-blocking depending onc.blocking)【channel.go‑L23, L99‑L124】. - The
c.closedsignal → gracefully closes all subscriber channels and exits【L15‑L21】.
3. Publish
Publish(ctx, msg) wraps the payload in a channelMessage and either sends it synchronously (publish) or asynchronously (publishNonBlocking) based on the blocking flag【L71‑L83】. The context allows the publisher to discard a message if cancelled early【L38‑L49】.
4. Subscribe and Unsubscribe
Subscribe() creates a new subscriber channel (respecting bufferSize and subsLimit) and appends it to the slice under a write lock【L44‑L53】. Unsubscribe() removes the channel safely under the same lock【L55‑L68】.
Real-Time Monitoring Data Flow
The stats channel facilitates live traffic monitoring through the following operational sequence:
- Configure: Users enable a channel in the Xray JSON/YAML configuration using parameters that mirror
ChannelConfig(blocking mode, subscriber limit, buffer size). - Manager Registration: When Xray-core boots,
stats.NewManagerregisters a default channel viaRegisterChanneland starts it if the manager is already running【stats.go‑L49】. - Publisher Emission: Any component collecting metrics (bytes transferred, connection counts) calls
manager.GetChannel("...").Publish(ctx, metricStruct). Because the channel runs its own goroutine, publishing never blocks the data path unless blocking mode is explicitly enabled. - Subscriber Consumption: External tools (e.g.,
xray api statsCLI or gRPC clients) callChannel.Subscribe()to receive a<-chan interface{}. As soon asPublishoccurs, the message forwards to every active subscriber, providing a live stream of traffic statistics.
The combination of a lock-protected subscriber list, optional non-blocking delivery, and context-aware cancellation ensures the monitoring pipeline remains lightweight and does not compromise proxy performance.
Practical Implementation Example
Below is a minimal Go example demonstrating how to create a manager, register a channel, publish a custom metric, and subscribe to the live stream.
package main
import (
"context"
"fmt"
"time"
"github.com/xtls/xray-core/app/stats"
)
type MyMetric struct {
Tag string
Bytes int64
}
func main() {
// 1️⃣ Initialise the stats manager
mgr, _ := stats.NewManager(context.Background(), &stats.Config{})
_ = mgr.Start() // Starts any pre-registered channels
// 2️⃣ Register a custom channel called "traffic"
ch, _ := mgr.RegisterChannel("traffic")
// 3️⃣ Subscribe to the channel (e.g., a monitoring goroutine)
sub, _ := ch.Subscribe()
go func() {
for msg := range sub {
if m, ok := msg.(MyMetric); ok {
fmt.Printf("[monitor] %s transferred %d bytes\n", m.Tag, m.Bytes)
}
}
}()
// 4️⃣ Simulate a publisher that emits metrics every second
ctx := context.Background()
go func() {
for i := 0; i < 5; i++ {
ch.Publish(ctx, MyMetric{Tag: "example.com", Bytes: int64(1024 * (i + 1))})
time.Sleep(time.Second)
}
}()
// Let the demo run briefly
time.Sleep(6 * time.Second)
_ = ch.Close()
}
Key implementation details demonstrated above:
- Channel creation uses
RegisterChannel, which internally callsNewChannelwith a default buffer of 64 and non-blocking mode. - Publishing is a one-liner:
ch.Publish(ctx, metric). ThechannelMessagewrapper ensures early-cancellation safety. - Subscription returns a
chan interface{}that can be read in a separate goroutine, providing real-time updates as soon asPublishis called.
Configuring Stats Channels in Xray-core
Users enable and tune channels via the main configuration file. The following YAML example shows how to configure a non-blocking channel with custom buffer sizes:
stats:
enabled: true
channels:
traffic:
blocking: false # non-blocking delivery (default)
subscriber_limit: 0 # 0 = unlimited subscribers
buffer_size: 128 # size of each subscriber's buffer
The blocking field determines whether publishers wait for the channel goroutine to accept the message (blocking) or drop messages when buffers are full (non-blocking). The subscriber_limit prevents memory exhaustion from uncontrolled subscriber growth.
Key Source Files in Xray-core
Understanding these files is essential for extending or debugging the stats system:
app/stats/channel.go: Core implementation of the publish-subscribe channel, including concurrency handling and optional blocking modes.app/stats/config.proto: Protobuf definition for channel tuning parameters (Blocking,SubscriberLimit,BufferSize).app/stats/stats.go: Manager that registers, starts, and shuts down channels; bridges the channel with the rest of Xray’s stats framework.app/stats/command/command.proto: gRPC definition used by external tools (e.g.,xray api stats) to subscribe to the live stats channel.app/stats/channel_test.go: Unit tests demonstrating basic subscription and publishing workflows.
Summary
- Xray-core stats channels provide a thread-safe, decoupled mechanism for real-time traffic monitoring using a publish-subscribe pattern.
- The
stats.Channelimplementation inapp/stats/channel.gomanages subscriber lists with mutex protection and supports both blocking and non-blocking delivery modes. - Configuration via
ChannelConfigallows tuning of buffer sizes and subscriber limits to balance observability with memory constraints. - External integration is available through gRPC commands defined in
app/stats/command/command.proto, enabling tools likexray apito consume live metric streams. - The architecture ensures that metric collection never blocks the core proxy data path, maintaining high-throughput performance under load.
Frequently Asked Questions
What is the difference between blocking and non-blocking mode in Xray-core stats channels?
In blocking mode, the Publish call waits synchronously until the channel goroutine accepts the message, ensuring zero message loss at the cost of potential latency in the publisher. In non-blocking mode (the default), Publish returns immediately; if the internal buffer is full, the message is dropped. This prevents the proxy data path from stalling when monitoring consumers are slow, as implemented in channel.go【L71‑L83】.
How do external tools subscribe to Xray-core traffic statistics?
External tools call the Subscribe() method on a channel obtained via the stats manager, which returns a receive-only Go channel (<-chan interface{}). Real-world implementations typically use the gRPC interface defined in app/stats/command/command.proto, where the xray api stats CLI acts as a subscriber that receives serialized metric messages from the running Xray-core instance.
Can I configure multiple independent stats channels for different metric types?
Yes. The stats manager in app/stats/stats.go supports registering multiple named channels via RegisterChannel(name). You can configure separate channels for distinct concerns (e.g., "traffic_bytes", "connection_latency", "reject_counts") each with independent buffer sizes and subscriber limits, allowing fine-grained control over resource allocation per metric stream.
What happens to subscribers when a channel is closed?
When Close() is invoked on a channel, the internal c.closed signal triggers the broadcast goroutine to exit gracefully【channel.go‑L21】. All active subscriber channels are then closed, causing any goroutines ranging over <-chan interface{} to receive the zero value and exit their loops. This ensures no goroutine leaks occur during shutdown or configuration reloads.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →