wsstat

package module
v3.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 29, 2026 License: MIT Imports: 19 Imported by: 0

README

wsstat

Go Documentation Go Report Card MIT License

This project provides a way to stat a WebSocket connection; measure the latency and learn the transport details. It implements the Go package wsstat and a CLI tool, also named wsstat.

wsstat CLI Client

The CLI client provides a simple and easy to use tool to check the status of a WebSocket endpoint:

~ wsstat example.org

Target: example.org
IP: 1.2.3.4
WS version: 13
TLS version: TLS 1.3

  DNS Lookup    TCP Connection    TLS Handshake    WS Handshake    Message RTT
|     61ms  |           22ms  |          44ms  |         29ms  |        27ms  |
|           |                 |                |               |              |
|  DNS lookup:61ms            |                |               |              |
|                 TCP connected:84ms           |               |              |
|                                       TLS done:128ms         |              |
|                                                        WS done:158ms        |
-                                                                         Total:186ms

The client replicates what reorx/httpstat and davecheney/httpstat does for HTTP, but for WebSocket. It is said that imitation is the sincerest form of flattery, and inspiration has for certain been sourced from these projects.

Install
Snap

If you are using a Linux distribution that supports Snap, you can install the tool from the Snap Store:

sudo snap install wsstat
Go

Requires that you have Go installed on your system and that you have $GOPATH/bin in your PATH. Recommended Go version is 1.26 or later.

Install via Go:

# To install the latest version, specify other releases with @<tag>
go install github.com/jkbrsn/wsstat/v3@latest

# To include the version in the binary, run the install from the root of the repo
git clone github.com/jkbrsn/wsstat
cd wsstat
git fetch --all
git checkout origin/main
go install -ldflags "-X main.version=$(cat VERSION)" ./cmd/wsstat

Note: installing the package with @latest will always install the latest version no matter the other parameters of the command.

The snap is listed here: snapcraft.io/wsstat

Maintainers: see docs/operations/snap-release-flow.md for how snap revisions are built, published to edge, and promoted.

Binary

Prebuilt release binaries are currently published for Linux/amd64 only. On other platforms, build from source (see macOS and Windows below).

Linux

Download the binary from the latest release (amd64) on the release page:

wget https://github.com/jkbrsn/wsstat/releases/download/<tag>/wsstat-<tag>

Make the binary executable:

chmod +x wsstat

Move the binary to a directory in your PATH:

sudo mv wsstat /usr/local/bin/wsstat  # system-wide
mv wsstat ~/bin/wsstat  # user-specific, ensure ~/bin is in your PATH
macOS and Windows

Currently not actively supported, but you may build and try it yourself:

git clone https://github.com/jkbrsn/wsstat.git
cd wsstat
make build-all

# Binary ends up in ./bin/wsstat-<OS>-<ARCH>

Then for Windows:

  1. Place the binary in a directory of your choice and add the directory to your PATH environment variable.
  2. Rename the binary to wsstat.exe for convenience.
  3. You should now be able to run wsstat from the command prompt or PowerShell.

For macOS:

  1. Make the binary executable: chmod +x wsstat-darwin-<ARCH>
  2. Move the binary to a directory in your PATH: sudo mv wsstat-darwin-<ARCH> /usr/local/bin/wsstat
Usage

Some examples:

# Basic request
wsstat wss://echo.example.com

# Send a text message
wsstat -t "ping" wss://echo.example.com

# Read the payload from a file or stdin (@file / @-)
wsstat -t @payload.json wss://echo.example.com
echo '{"hello":"world"}' | wsstat -t @- wss://echo.example.com

# Send an RPC method (JSON-RPC 2.0 by default; --rpc-version 1.0 for legacy servers)
wsstat --rpc-method eth_blockNumber wss://rpc.example.com/ws

# Start a stream
wsstat stream --summary-interval 5s wss://stream.example.com/feed

# Attach headers to dial request
wsstat -H "Authorization: Bearer TOKEN" -H "Origin: https://foo" wss://api.example.com/ws

# Resolve to a target IP and set a longer timeout
wsstat --resolve example.com:443:127.0.0.1 --timeout 30s wss://example.com/ws

# Allow insecure connection, make output extra verbose
wsstat --insecure -vv wss://self-signed.example.com

For a full list of the available options, run wsstat -h, wsstat measure -h, or wsstat stream -h.

Modes

wsstat <url> (or wsstat measure <url>) measures connection latency. wsstat stream <url> keeps the socket open for long-lived feeds. A host literally named measure/stream must be spelled with a scheme (wsstat wss://stream).

In measure mode, -c N aggregates timing across all interactions; the response printed is the first one (measure does not concatenate responses).

Stream Mode

Long-lived streaming endpoints are exercised with the stream subcommand:

wsstat stream -t '{"method":"subscribe"}' wss://example.org/stream

stream keeps the socket open, forwards each incoming frame to stdout, and periodically snapshots timing metrics. Use -b/--buffer to adjust the per-subscription queue length and --summary-interval (for example, 30s) to print recurring summaries that include per-subscription message counts, byte totals, and mean inter-arrival latency.

measure defaults to -c 1. In stream, -c 0 (the default) keeps the connection open until you cancel it, while any positive value limits delivery to that many events before wsstat disconnects:

wsstat stream -c 5 -t '{"method":"subscribe"}' wss://example.org/stream

For a single-response probe, run stream --once, which streams and exits after the first event:

wsstat stream --once -t '{"method":"subscribe_ticker"}' wss://example.org/ws
Output

Output is split across three orthogonal axes:

  • -o, --output text|json|raw — the whole-stdout contract. json emits newline-delimited envelopes with a stable schema (-v/-vv never change which fields appear); raw writes payload bytes verbatim with nothing added — no label, color, or trailing newline, so frames stay binary-safe and stream frames concatenate undelimited (use -o json when you need a delimiter). In measure mode raw requires --text or --rpc-method; with --rpc-method the response frame is decoded before output, so raw emits compact JSON rather than byte-for-byte wire content.
  • --body auto|compact — human body rendering (text output only). auto pretty-prints; compact puts each message on one line.
  • --clip — clips each rendered line to the terminal width with a trailing ... (text output, TTY only; a no-op when piped or redirected).
# Machine-readable streaming summaries and events
wsstat stream -o json --summary-interval 5s wss://example.org/stream

# Scannable one-line-per-event output, clipped to the terminal
wsstat stream --body compact --clip wss://example.org/stream

--body, --clip, -q, -v, and -vv apply only to -o text and are rejected under -o json|raw.

Exit Codes
Code Meaning
0 Success (also --help and --version)
1 Runtime failure (dial, measurement, stream, or output write)
2 Usage error (bad flag or argument)
130 Interrupted; a second Ctrl-C forces teardown

Usage errors (exit 2) always print plain text to stderr. Under -o json, a runtime failure (exit 1) prints a {"type":"error"} envelope to stdout so a wsstat ... -o json | jq pipeline stays parseable on the failure path:

wsstat -o json ws://127.0.0.1:1
# {"schema_version":"1.0","type":"error","error":"measuring latency: ..."}

wsstat Library Package

Use the wsstat Golang package to trace WebSocket connection and latency in your Go applications. It wraps coder/websocket for the WebSocket protocol implementation, and measures the duration of the different phases of the connection cycle.

Install

Install to use in your Go project:

go get github.com/jkbrsn/wsstat/v3
Usage

The examples/main.go program demonstrates two ways to use the wsstat package to trace a WebSocket connection. The example only executes one-hit message reads and writes, but WSStat also support operating on a continuous connection.

Run the example like this, from project root:

go run examples/main.go <a WebSocket URL>

Build & Test

The project has a Makefile that provides a number of commands to build and test the project:

# build
make build
make build-all  # build for all supported platforms
make clean      # remove built binaries and clear the golangci-lint cache

# test
make test
make test V=1 RACE=1  # test with optional flags

# lint
make lint

Contributing

For contributions, please open a GitHub issue with questions or suggestions. Before submitting an issue, have a look at the existing TODO list to see if what you've got in mind is already in the works.

Documentation

Overview

Package wsstat measures the latency of WebSocket connections. It wraps the coder/websocket package and includes latency measurements in the Result struct.

Index

Constants

View Source
const (

	// TextMessage denotes a UTF-8 encoded text message (e.g. JSON). Numerically identical to
	// websocket.MessageText so the public int-based API stays stable across the transport swap.
	TextMessage = 1
	// BinaryMessage denotes a binary data message. Numerically identical to
	// websocket.MessageBinary.
	BinaryMessage = 2
)

Variables

View Source
var (
	// ErrConnectionNotEstablished is returned when a read or ping is attempted before a
	// successful dial.
	ErrConnectionNotEstablished = errors.New("wsstat: connection not established")
	// ErrClosed is returned when an operation is attempted on a closed connection.
	ErrClosed = errors.New("wsstat: connection closed")
)

Exported error sentinels for the failure classes library consumers branch on. Returned (via errors.Is) by the connection methods; the one-shot Measure* functions propagate them.

Functions

This section is empty.

Types

type CertificateDetails

type CertificateDetails struct {
	CommonName         string
	Issuer             string
	NotBefore          time.Time
	NotAfter           time.Time
	PublicKeyAlgorithm x509.PublicKeyAlgorithm
	SignatureAlgorithm x509.SignatureAlgorithm
	DNSNames           []string
	IPAddresses        []net.IP
	URIs               []*url.URL
}

CertificateDetails holds details regarding a certificate.

type Option

type Option func(*options)

Option configures a WSStat instance.

func WithBufferSize

func WithBufferSize(n int) Option

WithBufferSize sets the buffer size for read/write/pong channels.

func WithCloseGrace

func WithCloseGrace(d time.Duration) Option

WithCloseGrace bounds how long Close waits for the peer's closing-handshake echo before forcing the connection shut. Zero or negative fires the teardown immediately rather than granting a grace window; a peer that echoes in that instant may still complete the handshake cleanly. Defaults to 3s.

Only values below 5s take effect: the underlying coder/websocket library caps its own close handshake at a hard-coded 5s, so Close returns by then regardless and a larger grace cannot extend the wait. The useful range is (0, 5s).

func WithCompression

func WithCompression(enabled bool) Option

WithCompression enables negotiation of the permessage-deflate extension. Disabled by default. The negotiated extension is reported in Result.Compression.

func WithHeaders

func WithHeaders(headers http.Header) Option

WithHeaders sets HTTP headers merged into every handshake request. Headers passed directly to Dial/DialContext take precedence over these on a per-key basis.

func WithLogger

func WithLogger(logger zerolog.Logger) Option

WithLogger sets the logger for the WSStat instance.

func WithReadLimit

func WithReadLimit(n int64) Option

WithReadLimit bounds the size in bytes of a single inbound message. A value of 0 keeps the default (16 MiB); a negative value disables the limit (use with care: an unbounded message can exhaust memory). Defaults to 16 MiB.

func WithResolves

func WithResolves(resolves map[string]string) Option

WithResolves sets DNS resolution overrides for specific host:port combinations. Map key format: "host:port", value: "ip_address".

func WithSubprotocols

func WithSubprotocols(subprotocols []string) Option

WithSubprotocols sets the WebSocket subprotocols to offer during the handshake, in preference order. The negotiated value is reported in Result.Subprotocol.

func WithTLSConfig

func WithTLSConfig(cfg *tls.Config) Option

WithTLSConfig sets the TLS configuration for the connection.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the timeout used for dialing and read deadlines.

func WithValidateUTF8

func WithValidateUTF8(enabled bool) Option

WithValidateUTF8 enables UTF-8 validation of inbound text frames. coder/websocket performs none (RFC 6455 §5.6 requires text payloads to be valid UTF-8); when enabled, an invalid text frame is logged at warn level and counted in Result.InvalidUTF8Frames rather than failing the connection. Disabled by default.

type Result

type Result struct {
	IPs             []string             // IP addresses of the WebSocket connection
	URL             *url.URL             // URL of the WebSocket connection
	Subprotocol     string               // Negotiated WebSocket subprotocol ("" if none)
	Compression     string               // Negotiated Sec-WebSocket-Extensions value ("" if none)
	RequestHeaders  http.Header          // Headers of the initial request
	ResponseHeaders http.Header          // Headers of the response
	TLSState        *tls.ConnectionState // State of the TLS connection
	MessageCount    int                  // Number of messages sent and received

	// InvalidUTF8Frames counts inbound text frames that failed UTF-8 validation. Always 0
	// unless WithValidateUTF8 is set (coder/websocket does not validate text frames itself).
	InvalidUTF8Frames int

	// Subscription statistics captured when long-lived streams are active.
	Subscriptions          map[string]SubscriptionStats // Metrics by subscription ID
	SubscriptionFirstEvent time.Duration                // Time until first subscription event
	SubscriptionLastEvent  time.Duration                // Time until last subscription event

	// Duration of each phase of the connection
	DNSLookup     time.Duration // Time to resolve DNS
	TCPConnection time.Duration // TCP connection establishment time
	TLSHandshake  time.Duration // Time to perform TLS handshake
	WSHandshake   time.Duration // Time to perform WebSocket handshake
	MessageRTT    time.Duration // Time to send message and receive response

	// Cumulative durations over the connection timeline
	DNSLookupDone        time.Duration // Time to resolve DNS (might be redundant with DNSLookup)
	TCPConnected         time.Duration // Time until the TCP connection is established
	TLSHandshakeDone     time.Duration // Time until the TLS handshake is completed
	WSHandshakeDone      time.Duration // Time until the WS handshake is completed
	FirstMessageResponse time.Duration // Time until the first message is received
	TotalTime            time.Duration // Total time from opening to closing the connection
}

Result holds durations of each phase of a WebSocket connection, cumulative durations over the connection timeline, and other relevant connection details.

func MeasureJSON

func MeasureJSON(
	ctx context.Context, target *url.URL, msgs []any, opts ...Option,
) (*Result, []any, error)

MeasureJSON behaves like MeasureText but encodes each element of msgs as JSON and decodes each response as JSON. Settings are supplied via opts.

func MeasurePing

func MeasurePing(
	ctx context.Context, target *url.URL, count int, opts ...Option,
) (*Result, error)

MeasurePing establishes a connection to target, performs count ping/pong round-trips, closes the connection, and returns the measured Result. Settings are supplied via opts.

func MeasureText

func MeasureText(
	ctx context.Context, target *url.URL, msgs []string, opts ...Option,
) (*Result, []string, error)

MeasureText establishes a WebSocket connection to target, sends each message in msgs as a text frame, reads one response per message, closes the connection, and returns the measured Result together with the responses in order. A single message is a one-element slice. Custom headers and other settings are supplied via opts (e.g. WithHeaders, WithTimeout). The whole operation honors ctx; a nil or already-canceled ctx aborts before dialing.

func (*Result) CertificateDetails

func (r *Result) CertificateDetails() []CertificateDetails

CertificateDetails returns a slice of CertificateDetails for each certificate in the TLS connection.

func (*Result) Format

func (r *Result) Format(s fmt.State, verb rune)

Format formats the time.Duration members of Result.

type Subscription

type Subscription struct {
	ID string
	// contains filtered or unexported fields
}

Subscription captures a long-lived stream registered through Subscribe. Counters are updated atomically by the WSStat instance.

func (*Subscription) ByteCount

func (s *Subscription) ByteCount() uint64

ByteCount reports the aggregate payload size delivered to the subscription.

func (*Subscription) Cancel

func (s *Subscription) Cancel()

Cancel stops the subscription and prevents further deliveries.

func (*Subscription) Done

func (s *Subscription) Done() <-chan struct{}

Done returns a channel that closes once the subscription is fully torn down.

func (*Subscription) MessageCount

func (s *Subscription) MessageCount() uint64

MessageCount reports the total number of messages delivered to the subscription.

func (*Subscription) Unsubscribe

func (s *Subscription) Unsubscribe()

Unsubscribe is an alias for Cancel and preserves semantic clarity for callers.

func (*Subscription) Updates

func (s *Subscription) Updates() <-chan SubscriptionMessage

Updates exposes the buffered stream of subscription messages.

type SubscriptionMessage

type SubscriptionMessage struct {
	MessageType int
	Data        []byte
	Decoded     any
	Received    time.Time
	Err         error
	Size        int
}

SubscriptionMessage represents a single frame delivered to a subscription consumer.

type SubscriptionOptions

type SubscriptionOptions struct {
	// ID can be provided to preassign a human-readable identifier. If empty, WSStat
	// allocates an incremental identifier.
	ID string

	// MessageType and Payload describe the initial frame sent to initiate the subscription.
	MessageType int
	Payload     []byte

	// Buffer controls the per-subscription delivery queue length. Zero implies the default.
	Buffer int
	// contains filtered or unexported fields
}

SubscriptionOptions configures how WSStat establishes and demultiplexes a subscription.

type SubscriptionStats

type SubscriptionStats struct {
	FirstEvent       time.Duration
	LastEvent        time.Duration
	MessageCount     uint64
	ByteCount        uint64
	MeanInterArrival time.Duration
	Error            error
}

SubscriptionStats snapshots per-subscription metrics for reporting through Result.

type WSStat

type WSStat struct {
	// contains filtered or unexported fields
}

WSStat wraps the coder/websocket package with latency measuring capabilities.

Concurrency: DialContext must complete (single-threaded) before any other method is called. After a successful dial, ExtractResult and Close are safe to call concurrently with each other and with the read/write/subscription methods; ExtractResult takes a consistent snapshot even while Close is finalizing the Result. Close is idempotent and may be called from multiple goroutines. WriteMessage/WriteMessageJSON and ReadMessage/ReadMessageJSON/PingPong are not internally serialized against each other: concurrent writers (or concurrent readers) interleave on the shared channels, so callers that need ordering must coordinate it. Subscribe/SubscribeOnce are safe to call concurrently.

func New

func New(opts ...Option) *WSStat

New creates and returns a new WSStat instance. To adjust channel buffer size or timeouts, use options. If not provided, package defaults are used for compatibility.

func (*WSStat) Close

func (ws *WSStat) Close()

Close closes the WebSocket connection and cleans up the WSStat instance. Sets result times: CloseDone

func (*WSStat) CloseWith

func (ws *WSStat) CloseWith(code int, reason string) error

CloseWith closes the connection sending a chosen close code and reason in the RFC 6455 closing handshake, instead of Close's default StatusNormalClosure (1000) with an empty reason. code must be a sendable close status (1000-1003, 1007-1011, or 3000-4999) and reason at most 123 bytes (the control-frame payload limit minus the 2-byte code); an invalid code or over-long reason returns an error without closing. Returns ErrClosed if the connection is already closing. Otherwise teardown proceeds exactly as Close, which it calls; like Close it is idempotent, but the code/reason only take effect when CloseWith wins the race to initiate the close.

func (*WSStat) DialContext

func (ws *WSStat) DialContext(
	ctx context.Context, targetURL *url.URL, customHeaders http.Header,
) error

DialContext establishes a new WebSocket connection bound to ctx. Canceling ctx (or calling Close) tears down the connection and unblocks in-flight reads and writes. If required, specify custom headers to merge with the default headers. Sets times: dialStart, wsHandshakeDone

func (*WSStat) ExtractResult

func (ws *WSStat) ExtractResult() *Result

ExtractResult calculate the current results and returns a copy of the Result object. Safe to call concurrently with the read/write/subscription methods and with Close.

func (*WSStat) PingPong

func (ws *WSStat) PingPong() error

PingPong sends a ping through the WebSocket connection and blocks until the matching pong is received. coder's Ping is a synchronous round-trip, so both the write and read timings are recorded around the single call. Sets result times: MessageReads, MessageWrites

func (*WSStat) ReadMessage

func (ws *WSStat) ReadMessage() (int, []byte, error)

ReadMessage reads a message from the WebSocket connection and measures the round-trip time. If an error occurs, it will be returned. Sets time: MessageReads

func (*WSStat) ReadMessageJSON

func (ws *WSStat) ReadMessageJSON() (any, error)

ReadMessageJSON reads a message from the WebSocket connection and measures the round-trip time. Sets time: MessageReads

func (*WSStat) Subscribe

func (ws *WSStat) Subscribe(ctx context.Context, opts SubscriptionOptions) (*Subscription, error)

Subscribe registers a long-lived listener using the supplied options and context. The returned Subscription can be used to consume streamed frames until cancellation.

func (*WSStat) SubscribeOnce

func (ws *WSStat) SubscribeOnce(
	ctx context.Context,
	opts SubscriptionOptions,
) (SubscriptionMessage, error)

SubscribeOnce registers a subscription and waits for the first delivered message before canceling the subscription. The returned message is a snapshot of the first delivery.

func (*WSStat) WriteMessage

func (ws *WSStat) WriteMessage(messageType int, data []byte)

WriteMessage sends a message through the WebSocket connection. Sets time: MessageWrites

func (*WSStat) WriteMessageJSON

func (ws *WSStat) WriteMessageJSON(v any)

WriteMessageJSON sends a message through the WebSocket connection. Sets time: MessageWrites

Directories

Path Synopsis
cmd
wsstat command
Package main implements the wsstat command-line tool for measuring WebSocket connection latency and streaming subscription events.
Package main implements the wsstat command-line tool for measuring WebSocket connection latency and streaming subscription events.
Package main provides examples of how to use the wsstat package.
Package main provides examples of how to use the wsstat package.
internal
app
Package app provides a high-level client for measuring WebSocket latency and streaming subscription events.
Package app provides a high-level client for measuring WebSocket latency and streaming subscription events.
app/color
Package color provides ANSI color support for terminal output.
Package color provides ANSI color support for terminal output.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL